开源项目

开源项目

Go netpoll I/O 多路复用构建原生网络模型之源码深度解析

文章分享panjf2000 发表了文章 • 0 个评论 • 311 次浏览 • 4 天前 • 来自相关话题

原文Go netpoll I/O 多路复用构建原生网络模型之源码深度解析 ...查看全部

原文

Go netpoll I/O 多路复用构建原生网络模型之源码深度解析

导言

Go 基于 I/O multiplexing 和 goroutine 构建了一个简洁而高性能的原生网络模型(基于 Go 的I/O 多路复用 netpoll),提供了 goroutine-per-connection 这样简单的网络编程模式。在这种模式下,开发者使用的是同步的模式去编写异步的逻辑,极大地降低了开发者编写网络应用时的心智负担,且借助于 Go runtime scheduler 对 goroutines 的高效调度,这个原生网络模型不论从适用性还是性能上都足以满足绝大部分的应用场景。

然而,在工程性上能做到如此高的普适性和兼容性,最终暴露给开发者提供接口/模式如此简洁,其底层必然是基于非常复杂的封装,做了很多取舍,也有可能放弃了一些『极致』的设计和理念。事实上netpoll底层就是基于 epoll/kqueue/iocp 这些系统调用来做封装的,最终暴露出 goroutine-per-connection 这样的极简的开发模式给使用者。

Go netpoll 在不同的操作系统,其底层使用的 I/O 多路复用技术也不一样,可以从 Go 源码目录结构和对应代码文件了解 Go 在不同平台下的网络 I/O 模式的实现。比如,在 Linux 系统下基于 epoll,freeBSD 系统下基于 kqueue,以及 Windows 系统下基于 iocp。

本文将基于 linux 平台来解析 Go netpoll 之 I/O 多路复用的底层是如何基于 epoll 封装实现的,从源码层层推进,全面而深度地解析 Go netpoll 的设计理念和实现原理,以及 Go 是如何利用netpoll来构建它的原生网络模型的。主要涉及到的一些概念:I/O 模式、用户/内核空间、epoll、linux 源码、goroutine scheduler 等等,我会尽量简单地讲解,如果有对相关概念不熟悉的同学,还是希望能提前熟悉一下。

用户空间与内核空间

现在操作系统都是采用虚拟存储器,那么对 32 位操作系统而言,它的寻址空间(虚拟存储空间)为 4G(2 的 32 次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对 linux 操作系统而言,将最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF),供内核使用,称为内核空间,而将较低的 3G 字节(从虚拟地址 0x00000000 到0xBFFFFFFF),供各个进程使用,称为用户空间。

I/O 多路复用

在神作《UNIX 网络编程》里,总结归纳了 5 种 I/O 模型,包括同步和异步 I/O:

  • 阻塞 I/O (Blocking I/O)
  • 非阻塞 I/O (Nonblocking I/O)
  • I/O 多路复用 (I/O multiplexing)
  • 信号驱动 I/O (Signal driven I/O)
  • 异步 I/O (Asynchronous I/O)

操作系统上的 I/O 是用户空间和内核空间的数据交互,因此 I/O 操作通常包含以下两个步骤:

  1. 等待网络数据到达网卡(读就绪)/等待网卡可写(写就绪) –> 读取/写入到内核缓冲区
  2. 从内核缓冲区复制数据 –> 用户空间(读)/从用户空间复制数据 -> 内核缓冲区(写)

而判定一个 I/O 模型是同步还是异步,主要看第二步:数据在用户和内核空间之间复制的时候是不是会阻塞当前进程,如果会,则是同步 I/O,否则,就是异步 I/O。基于这个原则,这 5 种 I/O 模型中只有一种异步 I/O 模型:Asynchronous I/O,其余都是同步 I/O 模型。

这 5 种 I/O 模型的对比如下:

所谓 I/O 多路复用指的就是 select/poll/epoll 这一系列的多路选择器:支持单一线程同时监听多个文件描述符(I/O事件),阻塞等待,并在其中某个文件描述符可读写时收到通知。 I/O 复用其实复用的不是 I/O 连接,而是复用线程,让一个 thread of control 能够处理多个连接(I/O 事件)。

select & poll

#include <sys/select.h>

/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

// 和 select 紧密结合的四个宏:
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

select 是 epoll 之前 linux 使用的 I/O 事件驱动技术。

理解 select 的关键在于理解 fd_set,为说明方便,取 fd_set 长度为 1 字节,fd_set 中的每一 bit 可以对应一个文件描述符 fd,则 1 字节长的 fd_set 最大可以对应 8 个 fd。select 的调用过程如下:

  1. 执行 FD_ZERO(&set), 则 set 用位表示是 0000,0000
  2. 若 fd=5, 执行 FD_SET(fd, &set); 后 set 变为 0001,0000(第5位置为1)
  3. 再加入 fd=2, fd=1,则 set 变为 0001,0011
  4. 执行 select(6, &set, 0, 0, 0) 阻塞等待
  5. 若 fd=1, fd=2 上都发生可读事件,则 select 返回,此时 set 变为 0000,0011 (注意:没有事件发生的 fd=5 被清空)

基于上面的调用过程,可以得出 select 的特点:

  • 可监控的文件描述符个数取决于 sizeof(fd_set) 的值。假设服务器上 sizeof(fd_set)=512,每 bit 表示一个文件描述符,则服务器上支持的最大文件描述符是 512*8=4096。fd_set的大小调整可参考 【原创】技术系列之 网络模型(二) 中的模型 2,可以有效突破 select 可监控的文件描述符上限
  • 将 fd 加入 select 监控集的同时,还要再使用一个数据结构 array 保存放到 select 监控集中的 fd,一是用于在 select 返回后,array 作为源数据和 fd_set 进行 FD_ISSET 判断。二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,则每次开始 select 前都要重新从 array 取得 fd 逐一加入(FD_ZERO最先),扫描 array 的同时取得 fd 最大值 maxfd,用于 select 的第一个参数
  • 可见 select 模型必须在 select 前循环 array(加 fd,取 maxfd),select 返回后循环 array(FD_ISSET判断是否有事件发生)

所以,select 有如下的缺点:

  1. 最大并发数限制:使用 32 个整数的 32 位,即 32*32=1024 来标识 fd,虽然可修改,但是有以下第 2, 3 点的瓶颈
  2. 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
  3. 性能衰减严重:每次 kernel 都需要线性扫描整个 fd_set,所以随着监控的描述符 fd 数量增长,其 I/O 性能会线性下降

poll 的实现和 select 非常相似,只是描述 fd 集合的方式不同,poll 使用 pollfd 结构而不是 select 的 fd_set 结构,poll 解决了最大文件描述符数量限制的问题,但是同样需要从用户态拷贝所有的 fd 到内核态,也需要线性遍历所有的 fd 集合,所以它和 select 只是实现细节上的区分,并没有本质上的区别。

epoll

epoll 是 linux kernel 2.6 之后引入的新 I/O 事件驱动技术,I/O 多路复用的核心设计是 1 个线程处理所有连接的等待消息准备好I/O 事件,这一点上 epoll 和 select&poll 是大同小异的。但 select&poll 预估错误了一件事,当数十万并发连接存在时,可能每一毫秒只有数百个活跃的连接,同时其余数十万连接在这一毫秒是非活跃的。select&poll 的使用方法是这样的:返回的活跃连接 == select(全部待监控的连接)

什么时候会调用 select&poll 呢?在你认为需要找出有报文到达的活跃连接时,就应该调用。所以,select&poll 在高并发时是会被频繁调用的。这样,这个频繁调用的方法就很有必要看看它是否有效率,因为,它的轻微效率损失都会被高频二字所放大。它有效率损失吗?显而易见,全部待监控连接是数以十万计的,返回的只是数百个活跃连接,这本身就是无效率的表现。被放大后就会发现,处理并发上万个连接时,select&poll 就完全力不从心了。这个时候就该 epoll 上场了,epoll 通过一些新的设计和优化,基本上解决了 select&poll 的问题。

epoll 的 API 非常简洁,涉及到的只有 3 个系统调用:

#include <sys/epoll.h>  
int epoll_create(int size); // int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

其中,epoll_create 创建一个 epoll 实例并返回 epollfd;epoll_ctl 注册 file descriptor 等待的 I/O 事件(比如 EPOLLIN、EPOLLOUT 等) 到 epoll 实例上;epoll_wait 则是阻塞监听 epoll 实例上所有的 file descriptor 的 I/O 事件,它接收一个用户空间上的一块内存地址 (events 数组),kernel 会在有 I/O 事件发生的时候把文件描述符列表复制到这块内存地址上,然后 epoll_wait 解除阻塞并返回,最后用户空间上的程序就可以对相应的 fd 进行读写了:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

epoll 的工作原理如下:

与 select&poll 相比,epoll 分清了高频调用和低频调用。例如,epoll_ctl 相对来说就是不太频繁被调用的,而 epoll_wait 则是非常频繁被调用的。所以 epoll 利用 epoll_ctl 来插入或者删除一个 fd,实现用户态到内核态的数据拷贝,这确保了每一个 fd 在其生命周期只需要被拷贝一次,而不是每次调用 epoll_wait 的时候都拷贝一次。 epoll_wait 则被设计成几乎没有入参的调用,相比 select&poll 需要把全部监听的 fd 集合从用户态拷贝至内核态的做法,epoll 的效率就高出了一大截。

在实现上 epoll 采用红黑树来存储所有监听的 fd,而红黑树本身插入和删除性能比较稳定,时间复杂度 O(logN)。通过 epoll_ctl 函数添加进来的 fd 都会被放在红黑树的某个节点内,所以,重复添加是没有用的。当把 fd 添加进来的时候时候会完成关键的一步:该 fd 都会与相应的设备(网卡)驱动程序建立回调关系,也就是在内核中断处理程序为它注册一个回调函数,在 fd 相应的事件触发(中断)之后(设备就绪了),内核就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback这个回调函数其实就是把这个 fd 添加到 rdllist 这个双向链表(就绪链表)中。epoll_wait 实际上就是去检查 rdlist 双向链表中是否有就绪的 fd,当 rdlist 为空(无就绪fd)时挂起当前进程,直到 rdlist 非空时进程才被唤醒并返回。

相比于 select&poll 调用时会将全部监听的 fd 从用户态空间拷贝至内核态空间并线性扫描一遍找出就绪的 fd 再返回到用户态,epoll_wait 则是直接返回已就绪 fd,因此 epoll 的 I/O 性能不会像 select&poll 那样随着监听的 fd 数量增加而出现线性衰减,是一个非常高效的 I/O 事件驱动技术。

由于使用 epoll 的 I/O 多路复用需要用户进程自己负责 I/O 读写,从用户进程的角度看,读写过程是阻塞的,所以 select&poll&epoll 本质上都是同步 I/O 模型,而像 Windows 的 IOCP 这一类的异步 I/O,只需要在调用 WSARecv 或 WSASend 方法读写数据的时候把用户空间的内存 buffer 提交给 kernel,kernel 负责数据在用户空间和内核空间拷贝,完成之后就会通知用户进程,整个过程不需要用户进程参与,所以是真正的异步 I/O。

延伸

另外,我看到有些文章说 epoll 之所以性能高是因为利用了 linux 的 mmap 内存映射让内核和用户进程共享了一片物理内存,用来存放就绪 fd 列表和它们的数据 buffer,所以用户进程在 epoll_wait返回之后用户进程就可以直接从共享内存那里读取/写入数据了,这让我很疑惑,因为首先看epoll_wait的函数声明:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

第二个参数:就绪事件列表,是需要在用户空间分配内存然后再传给epoll_wait的,如果内核会用 mmap 设置共享内存,直接传递一个指针进去就行了,根本不需要在用户态分配内存,多此一举。其次,内核和用户进程通过 mmap 共享内存是一件极度危险的事情,内核无法确定这块共享内存什么时候会被回收,而且这样也会赋予用户进程直接操作内核数据的权限和入口,非常容易出现大的系统漏洞,因此一般极少会这么做。所以我很怀疑 epoll 是不是真的在 linux kernel 里用了 mmap,我就去看了下最新版本(5.3.9)的 linux kernel 源码:

/*
* Implement the event wait interface for the eventpoll file. It is the kernel
* part of the user space epoll_wait(2).
*/

static int do_epoll_wait(int epfd, struct epoll_event __user *events,
int maxevents, int timeout)
{
// ...

/* Time to fish for events ... */
error = ep_poll(ep, events, maxevents, timeout);
}

// 如果 epoll_wait 入参时设定 timeout == 0, 那么直接通过 ep_events_available 判断当前是否有用户感兴趣的事件发生,如果有则通过 ep_send_events 进行处理
// 如果设置 timeout > 0,并且当前没有用户关注的事件发生,则进行休眠,并添加到 ep->wq 等待队列的头部;对等待事件描述符设置 WQ_FLAG_EXCLUSIVE 标志
// ep_poll 被事件唤醒后会重新检查是否有关注事件,如果对应的事件已经被抢走,那么 ep_poll 会继续休眠等待
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events, int maxevents, long timeout)
{
// ...

send_events:
/*
* Try to transfer events to user space. In case we get 0 events and
* there's still timeout left over, we go trying again in search of
* more luck.
*/


// 如果一切正常, 有 event 发生, 就开始准备数据 copy 给用户空间了
// 如果有就绪的事件发生,那么就调用 ep_send_events 将就绪的事件 copy 到用户态内存中,
// 然后返回到用户态,否则判断是否超时,如果没有超时就继续等待就绪事件发生,如果超时就返回用户态。
// 从 ep_poll 函数的实现可以看到,如果有就绪事件发生,则调用 ep_send_events 函数做进一步处理
if (!res && eavail &&
!(res = ep_send_events(ep, events, maxevents)) && !timed_out)
goto fetch_events;

// ...
}

// ep_send_events 函数是用来向用户空间拷贝就绪 fd 列表的,它将用户传入的就绪 fd 列表内存简单封装到
// ep_send_events_data 结构中,然后调用 ep_scan_ready_list 将就绪队列中的事件写入用户空间的内存;
// 用户进程就可以访问到这些数据进行处理
static int ep_send_events(struct eventpoll *ep,
struct epoll_event __user *events, int maxevents)
{
struct ep_send_events_data esed;

esed.maxevents = maxevents;
esed.events = events;
// 调用 ep_scan_ready_list 函数检查 epoll 实例 eventpoll 中的 rdllist 就绪链表,
// 并注册一个回调函数 ep_send_events_proc,如果有就绪 fd,则调用 ep_send_events_proc 进行处理
ep_scan_ready_list(ep, ep_send_events_proc, &esed, 0, false);
return esed.res;
}

// 调用 ep_scan_ready_list 的时候会传递指向 ep_send_events_proc 函数的函数指针作为回调函数,
// 一旦有就绪 fd,就会调用 ep_send_events_proc 函数
static __poll_t ep_send_events_proc(struct eventpoll *ep, struct list_head *head, void *priv)
{
// ...

/*
* If the event mask intersect the caller-requested one,
* deliver the event to userspace. Again, ep_scan_ready_list()
* is holding ep->mtx, so no operations coming from userspace
* can change the item.
*/

revents = ep_item_poll(epi, &pt, 1);
// 如果 revents 为 0,说明没有就绪的事件,跳过,否则就将就绪事件拷贝到用户态内存中
if (!revents)
continue;
// 将当前就绪的事件和用户进程传入的数据都通过 __put_user 拷贝回用户空间,
// 也就是调用 epoll_wait 之时用户进程传入的 fd 列表的内存
if (__put_user(revents, &uevent->events) || __put_user(epi->event.data, &uevent->data)) {
list_add(&epi->rdllink, head);
ep_pm_stay_awake(epi);
if (!esed->res)
esed->res = -EFAULT;
return 0;
}

// ...
}

do_epoll_wait开始层层跳转,我们可以很清楚地看到最后内核是通过__put_user函数把就绪 fd 列表和事件返回到用户空间,而__put_user正是内核用来拷贝数据到用户空间的标准函数。此外,我并没有在 linux kernel 的源码中和 epoll 相关的代码里找到 mmap 系统调用做内存映射的逻辑,所以基本可以得出结论:epoll 在 linux kernel 里并没有使用 mmap 来做用户空间和内核空间的内存共享,所以那些说 epoll 使用了 mmap 的文章都是误解。

Non-blocking I/O

什么叫非阻塞 I/O,顾名思义就是:所有 I/O 操作都是立刻返回而不会阻塞当前用户进程。I/O 多路复用通常情况下需要和非阻塞 I/O 搭配使用,否则可能会产生意想不到的问题。比如,epoll 的 ET(边缘触发) 模式下,如果不使用非阻塞 I/O,有极大的概率会导致阻塞 event-loop 线程,从而降低吞吐量,甚至导致 bug。

Linux 下,我们可以通过 fcntl 系统调用来设置 O_NONBLOCK标志位,从而把 socket 设置成 non-blocking。当对一个 non-blocking socket 执行读操作时,流程是这个样子:

当用户进程发出 read 操作时,如果 kernel 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个 EAGAIN error。从用户进程角度讲 ,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦 kernel 中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,non-blocking I/O 的特点是用户进程需要不断的主动询问 kernel 数据好了没有。

Go netpoll

一个典型的 Go TCP server:

package main

import (
"fmt"
"net"
)

func main() {
listen, err := net.Listen("tcp", ":8888")
if err != nil {
fmt.Println("listen error: ", err)
return
}

for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept error: ", err)
break
}

// start a new goroutine to handle the new connection
go HandleConn(conn)
}
}
func HandleConn(conn net.Conn) {
defer conn.Close()
packet := make([]byte, 1024)
for {
// 如果没有可读数据,也就是读 buffer 为空,则阻塞
_, _ = conn.Read(packet)
// 同理,不可写则阻塞
_, _ = conn.Write(packet)
}
}

上面是一个基于 Go 原生网络模型(基于 netpoll)编写的一个 TCP server,模式是 goroutine-per-connection,在这种模式下,开发者使用的是同步的模式去编写异步的逻辑而且对于开发者来说 I/O 是否阻塞是无感知的,也就是说开发者无需考虑 goroutines 甚至更底层的线程、进程的调度和上下文切换。而 Go netpoll 最底层的事件驱动技术肯定是基于 epoll/kqueue/iocp 这一类的 I/O 事件驱动技术,只不过是把这些调度和上下文切换的工作转移到了 runtime 的 Go scheduler,让它来负责调度 goroutines,从而极大地降低了程序员的心智负担!

Go netpoll 核心

Go netpoll 通过在底层对 epoll/kqueue/iocp 的封装,从而实现了使用同步编程模式达到异步执行的效果。总结来说,所有的网络操作都以网络描述符 netFD 为中心实现。netFD 与底层 PollDesc 结构绑定,当在一个 netFD 上读写遇到 EAGAIN 错误时,就将当前 goroutine 存储到这个 netFD 对应的 PollDesc 中,同时调用 gopark 把当前 goroutine 给 park 住,直到这个 netFD 上再次发生读写事件,才将此 goroutine 给 ready 激活重新运行。显然,在底层通知 goroutine 再次发生读写等事件的方式就是 epoll/kqueue/iocp 等事件驱动机制。

接下来我们通过分析最新的 Go 源码(v1.13.4),解读一下整个 netpoll 的运行流程。

上面的示例代码中相关的在源码里的几个数据结构和方法:

// TCPListener is a TCP network listener. Clients should typically
// use variables of type Listener instead of assuming TCP.
type TCPListener struct {
fd *netFD
lc ListenConfig
}

// Accept implements the Accept method in the Listener interface; it
// waits for the next call and returns a generic Conn.
func (l *TCPListener) Accept() (Conn, error) {
if !l.ok() {
return nil, syscall.EINVAL
}
c, err := l.accept()
if err != nil {
return nil, &OpError{Op: "accept", Net: l.fd.net, Source: nil, Addr: l.fd.laddr, Err: err}
}
return c, nil
}

func (ln *TCPListener) accept() (*TCPConn, error) {
fd, err := ln.fd.accept()
if err != nil {
return nil, err
}
tc := newTCPConn(fd)
if ln.lc.KeepAlive >= 0 {
setKeepAlive(fd, true)
ka := ln.lc.KeepAlive
if ln.lc.KeepAlive == 0 {
ka = defaultTCPKeepAlive
}
setKeepAlivePeriod(fd, ka)
}
return tc, nil
}

// TCPConn is an implementation of the Conn interface for TCP network
// connections.
type TCPConn struct {
conn
}

// Conn
type conn struct {
fd *netFD
}

type conn struct {
fd *netFD
}

func (c *conn) ok() bool { return c != nil && c.fd != nil }

// Implementation of the Conn interface.

// Read implements the Conn Read method.
func (c *conn) Read(b []byte) (int, error) {
if !c.ok() {
return 0, syscall.EINVAL
}
n, err := c.fd.Read(b)
if err != nil && err != io.EOF {
err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
}
return n, err
}

// Write implements the Conn Write method.
func (c *conn) Write(b []byte) (int, error) {
if !c.ok() {
return 0, syscall.EINVAL
}
n, err := c.fd.Write(b)
if err != nil {
err = &OpError{Op: "write", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
}
return n, err
}

netFD

net.Listen("tcp", ":8888") 方法返回了一个 TCPListener,它是一个实现了 net.Listener 接口的 struct,而通过 listen.Accept() 接收的新连接 TCPConn 则是一个实现了 net.Conn 接口的 struct,它内嵌了 net.conn struct。仔细阅读上面的源码可以发现,不管是 Listener 的 Accept 还是 Conn 的 Read/Write 方法,都是基于一个 netFD 的数据结构的操作,netFD 是一个网络描述符,类似于 Linux 的文件描述符的概念,netFD 中包含一个 poll.FD 数据结构,而 poll.FD 中包含两个重要的数据结构 Sysfd 和 pollDesc,前者是真正的系统文件描述符,后者对是底层事件驱动的封装,所有的读写超时等操作都是通过调用后者的对应方法实现的。

netFDpoll.FD的源码:

// Network file descriptor.
type netFD struct {
pfd poll.FD

// immutable until Close
family int
sotype int
isConnected bool // handshake completed or use of association with peer
net string
laddr Addr
raddr Addr
}

// FD is a file descriptor. The net and os packages use this type as a
// field of a larger type representing a network connection or OS file.
type FD struct {
// Lock sysfd and serialize access to Read and Write methods.
fdmu fdMutex

// System file descriptor. Immutable until Close.
Sysfd int

// I/O poller.
pd pollDesc

// Writev cache.
iovecs *[]syscall.Iovec

// Semaphore signaled when file is closed.
csema uint32

// Non-zero if this file has been set to blocking mode.
isBlocking uint32

// Whether this is a streaming descriptor, as opposed to a
// packet-based descriptor like a UDP socket. Immutable.
IsStream bool

// Whether a zero byte read indicates EOF. This is false for a
// message based socket connection.
ZeroReadIsEOF bool

// Whether this is a file rather than a network socket.
isFile bool
}

pollDesc

前面提到了 pollDesc 是底层事件驱动的封装,netFD 通过它来完成各种 I/O 相关的操作,它的定义如下:

type pollDesc struct {
runtimeCtx uintptr
}

这里的 struct 只包含了一个指针,而通过 pollDesc 的 init 方法,我们可以找到它具体的定义是在runtime.pollDesc这里:

func (pd *pollDesc) init(fd *FD) error {
serverInit.Do(runtime_pollServerInit)
ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd))
if errno != 0 {
if ctx != 0 {
runtime_pollUnblock(ctx)
runtime_pollClose(ctx)
}
return syscall.Errno(errno)
}
pd.runtimeCtx = ctx
return nil
}

// Network poller descriptor.
//
// No heap pointers.
//
//go:notinheap
type pollDesc struct {
link *pollDesc // in pollcache, protected by pollcache.lock

// The lock protects pollOpen, pollSetDeadline, pollUnblock and deadlineimpl operations.
// This fully covers seq, rt and wt variables. fd is constant throughout the PollDesc lifetime.
// pollReset, pollWait, pollWaitCanceled and runtime·netpollready (IO readiness notification)
// proceed w/o taking the lock. So closing, everr, rg, rd, wg and wd are manipulated
// in a lock-free way by all operations.
// NOTE(dvyukov): the following code uses uintptr to store *g (rg/wg),
// that will blow up when GC starts moving objects.
lock mutex // protects the following fields
fd uintptr
closing bool
everr bool // marks event scanning error happened
user uint32 // user settable cookie
rseq uintptr // protects from stale read timers
rg uintptr // pdReady, pdWait, G waiting for read or nil
rt timer // read deadline timer (set if rt.f != nil)
rd int64 // read deadline
wseq uintptr // protects from stale write timers
wg uintptr // pdReady, pdWait, G waiting for write or nil
wt timer // write deadline timer
wd int64 // write deadline
}

runtime.pollDesc包含自身类型的一个指针,用来保存下一个runtime.pollDesc的地址,以此来实现链表,可以减少数据结构的大小,所有的runtime.pollDesc保存在runtime.pollCache结构中,定义如下:

type pollCache struct {
lock mutex
first *pollDesc
// PollDesc objects must be type-stable,
// because we can get ready notification from epoll/kqueue
// after the descriptor is closed/reused.
// Stale notifications are detected using seq variable,
// seq is incremented when deadlines are changed or descriptor is reused.
}

net.Listen

调用 net.Listen之后,底层会通过 Linux 的系统调用socket 方法创建一个 fd 分配给 listener,并用以来初始化 listener 的 netFD,接着调用 netFD 的listenStream方法完成对 socket 的 bind&listen 操作以及对 netFD 的初始化(主要是对 netFD 里的 pollDesc 的初始化),相关源码如下:

// 调用 linux 系统调用 socket 创建 listener fd 并设置为为阻塞 I/O    
s, err := socketFunc(family, sotype|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, proto)
// On Linux the SOCK_NONBLOCK and SOCK_CLOEXEC flags were
// introduced in 2.6.27 kernel and on FreeBSD both flags were
// introduced in 10 kernel. If we get an EINVAL error on Linux
// or EPROTONOSUPPORT error on FreeBSD, fall back to using
// socket without them.

socketFunc func(int, int, int) (int, error) = syscall.Socket

// 用上面创建的 listener fd 初始化 listener netFD
if fd, err = newFD(s, family, sotype, net); err != nil {
poll.CloseFunc(s)
return nil, err
}

// 对 listener fd 进行 bind&listen 操作,并且调用 init 方法完成初始化
func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error {
// ...

// 完成绑定操作
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)
}

// 调用 init,内部会调用 poll.FD.Init,最后调用 pollDesc.init
if err = fd.init(); err != nil {
return err
}
lsa, _ = syscall.Getsockname(fd.pfd.Sysfd)
fd.setAddr(fd.addrFunc()(lsa), nil)
return nil
}

// 使用 sync.Once 来确保一个 listener 只持有一个 epoll 实例
var serverInit sync.Once

// netFD.init 会调用 poll.FD.Init 并最终调用到 pollDesc.init,
// 它会创建 epoll 实例并把 listener fd 加入监听队列
func (pd *pollDesc) init(fd *FD) error {
// runtime_pollServerInit 内部调用了 netpollinit 来创建 epoll 实例
serverInit.Do(runtime_pollServerInit)

// runtime_pollOpen 内部调用了 netpollopen 来将 listener fd 注册到
// epoll 实例中,另外,它会初始化一个 pollDesc 并返回
ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd))
if errno != 0 {
if ctx != 0 {
runtime_pollUnblock(ctx)
runtime_pollClose(ctx)
}
return syscall.Errno(errno)
}
// 把真正初始化完成的 pollDesc 实例赋值给当前的 pollDesc 代表自身的指针,
// 后续使用直接通过该指针操作
pd.runtimeCtx = ctx
return nil
}

// netpollopen 会被 runtime_pollOpen,注册 fd 到 epoll 实例,
// 同时会利用万能指针把 pollDesc 保存到 epollevent 的一个 8 位的字节数组 data 里
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)
}

我们前面提到的 epoll 的三个基本调用,Go 在源码里实现了对那三个调用的封装:

#include <sys/epoll.h>  
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

// Go 对上面三个调用的封装
func netpollinit()
func netpollopen(fd uintptr, pd *pollDesc) int32
func netpoll(block bool) gList

netFD 就是通过这三个封装来对 epoll 进行创建实例、注册 fd 和等待事件操作的。

Listener.Accept()

netpoll accept socket 的工作流程如下:

  1. 服务端的 netFD 在listen时会创建 epoll 的实例,并将 listenerFD 加入 epoll 的事件队列
  2. netFD 在accept时将返回的 connFD 也加入 epoll 的事件队列
  3. netFD 在读写时出现syscall.EAGAIN错误,通过 pollDesc 的 waitRead 方法将当前的 goroutine park 住,直到 ready,从 pollDesc 的waitRead中返回

Listener.Accept()接收来自客户端的新连接,具体还是调用netFD.accept方法来完成这个功能:

// Accept implements the Accept method in the Listener interface; it
// waits for the next call and returns a generic Conn.
func (l *TCPListener) Accept() (Conn, error) {
if !l.ok() {
return nil, syscall.EINVAL
}
c, err := l.accept()
if err != nil {
return nil, &OpError{Op: "accept", Net: l.fd.net, Source: nil, Addr: l.fd.laddr, Err: err}
}
return c, nil
}

func (ln *TCPListener) accept() (*TCPConn, error) {
fd, err := ln.fd.accept()
if err != nil {
return nil, err
}
tc := newTCPConn(fd)
if ln.lc.KeepAlive >= 0 {
setKeepAlive(fd, true)
ka := ln.lc.KeepAlive
if ln.lc.KeepAlive == 0 {
ka = defaultTCPKeepAlive
}
setKeepAlivePeriod(fd, ka)
}
return tc, nil
}

netFD.accept方法里再调用poll.FD.Accept,最后会使用 linux 的系统调用accept来完成新连接的接收,并且会把 accept 的 socket 设置成非阻塞 I/O 模式:

// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
if err := fd.readLock(); err != nil {
return -1, nil, "", err
}
defer fd.readUnlock()

if err := fd.pd.prepareRead(fd.isFile); err != nil {
return -1, nil, "", err
}
for {
// 使用 linux 系统调用 accept 接收新连接,创建对应的 socket
s, rsa, errcall, err := accept(fd.Sysfd)
// 因为 listener fd 在创建的时候已经设置成非阻塞的了,
// 所以 accept 方法会直接返回,不管有没有新连接到来;如果 err == nil 则表示正常建立新连接,直接返回
if err == nil {
return s, rsa, "", err
}
// 如果 err != nil,则判断 err == syscall.EAGAIN,符合条件则进入 pollDesc.waitRead 方法
switch err {
case syscall.EAGAIN:
if fd.pd.pollable() {
// 如果当前没有发生期待的 I/O 事件,那么 waitRead 会通过 park goroutine 让逻辑 block 在这里
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
}
}

// 使用 linux 的 accept 系统调用接收新连接并把这个 socket fd 设置成非阻塞 I/O
ns, sa, err := Accept4Func(s, syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC)
// On Linux the accept4 system call was introduced in 2.6.28
// kernel and on FreeBSD it was introduced in 10 kernel. If we
// get an ENOSYS error on both Linux and FreeBSD, or EINVAL
// error on Linux, fall back to using accept.

// Accept4Func is used to hook the accept4 call.
var Accept4Func func(int, int) (int, syscall.Sockaddr, error) = syscall.Accept4

pollDesc.waitRead方法主要负责检测当前这个 pollDesc 的上层 netFD 对应的 fd 是否有『期待的』I/O 事件发生,如果有就直接返回,否则就 park 住当前的 goroutine 并持续等待直至对应的 fd 上发生可读/可写或者其他『期待的』I/O 事件为止,然后它就会返回到外层的 for 循环,让 goroutine 继续执行逻辑。

Conn.Read/Conn.Write

我们先来看看Conn.Read方法是如何实现的,原理其实和 Listener.Accept 是一样的,具体调用链还是首先调用 conn 的netFD.Read,然后内部再调用 poll.FD.Read,最后使用 linux 的系统调用 read: syscall.Read完成数据读取:

// Implementation of the Conn interface.

// Read implements the Conn Read method.
func (c *conn) Read(b []byte) (int, error) {
if !c.ok() {
return 0, syscall.EINVAL
}
n, err := c.fd.Read(b)
if err != nil && err != io.EOF {
err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
}
return n, err
}

func (fd *netFD) Read(p []byte) (n int, err error) {
n, err = fd.pfd.Read(p)
runtime.KeepAlive(fd)
return n, wrapSyscallError("read", err)
}

// Read implements io.Reader.
func (fd *FD) Read(p []byte) (int, error) {
if err := fd.readLock(); err != nil {
return 0, err
}
defer fd.readUnlock()
if len(p) == 0 {
// If the caller wanted a zero byte read, return immediately
// without trying (but after acquiring the readLock).
// Otherwise syscall.Read returns 0, nil which looks like
// io.EOF.
// TODO(bradfitz): make it wait for readability? (Issue 15735)
return 0, nil
}
if err := fd.pd.prepareRead(fd.isFile); err != nil {
return 0, err
}
if fd.IsStream && len(p) > maxRW {
p = p[:maxRW]
}
for {
// 尝试从该 socket 读取数据,因为 socket 在被 listener accept 的时候设置成
// 了非阻塞 I/O,所以这里同样也是直接返回,不管有没有可读的数据
n, err := syscall.Read(fd.Sysfd, p)
if err != nil {
n = 0
// err == syscall.EAGAIN 表示当前没有期待的 I/O 事件发生,也就是 socket 不可读
if err == syscall.EAGAIN && fd.pd.pollable() {
// 如果当前没有发生期待的 I/O 事件,那么 waitRead
// 会通过 park goroutine 让逻辑 block 在这里
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}

// On MacOS we can see EINTR here if the user
// pressed ^Z. See issue #22838.
if runtime.GOOS == "darwin" && err == syscall.EINTR {
continue
}
}
err = fd.eofError(n, err)
return n, err
}
}

conn.Writeconn.Read的原理是一致的,它也是通过类似 pollDesc.waitReadpollDesc.waitWrite来 park 住 goroutine 直至期待的 I/O 事件发生才返回,而 pollDesc.waitWrite的内部实现原理和pollDesc.waitRead是一样的,都是基于runtime_pollWait,这里就不再赘述。

pollDesc.waitRead

pollDesc.waitRead内部调用了 runtime_pollWait来达成无 I/O 事件时 park 住 goroutine 的目的:

//go:linkname poll_runtime_pollWait internal/poll.runtime_pollWait
func poll_runtime_pollWait(pd *pollDesc, mode int) int {
err := netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}
// As for now only Solaris, illumos, and AIX use level-triggered IO.
if GOOS == "solaris" || GOOS == "illumos" || GOOS == "aix" {
netpollarm(pd, mode)
}
// 进入 netpollblock 并且判断是否有期待的 I/O 事件发生,
// 这里的 for 循环是为了一直等到 io ready
for !netpollblock(pd, int32(mode), false) {
err = netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}
// 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 0
}

// returns true if IO is ready, or false if timedout or closed
// waitio - wait only for completed IO, ignore errors
func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
// gpp 保存的是 goroutine 的数据结构 g,这里会根据 mode 的值决定是 rg 还是 wg
// 后面调用 gopark 之后,会把当前的 goroutine 的抽象数据结构 g 存入 gpp 这个指针
gpp := &pd.rg
if mode == 'w' {
gpp = &pd.wg
}

// set the gpp semaphore to WAIT
// 这个 for 循环是为了等待 io ready 或者 io wait
for {
old := *gpp
// gpp == pdReady 表示此时已有期待的 I/O 事件发生,
// 可以直接返回 unblock 当前 goroutine 并执行响应的 I/O 操作
if old == pdReady {
*gpp = 0
return true
}
if old != 0 {
throw("runtime: double wait")
}
// 如果没有期待的 I/O 事件发生,则通过原子操作把 gpp 的值置为 pdWait 并退出 for 循环
if atomic.Casuintptr(gpp, 0, pdWait) {
break
}
}

// need to recheck error states after setting gpp to WAIT
// this is necessary because runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl
// do the opposite: store to closing/rd/wd, membarrier, load of rg/wg

// waitio 此时是 false,netpollcheckerr 方法会检查当前 pollDesc 对应的 fd 是否是正常的,
// 通常来说 netpollcheckerr(pd, mode) == 0 是成立的,所以这里会执行 gopark
// 把当前 goroutine 给 park 住,直至对应的 fd 上发生可读/可写或者其他『期待的』I/O 事件为止,
// 然后 unpark 返回,在 gopark 内部会把当前 goroutine 的抽象数据结构 g 存入
// gpp(pollDesc.rg/pollDesc.wg) 指针里,以便在后面的 netpoll 函数取出 pollDesc 之后,
// 把 g 添加到链表里返回,然后重新调度运行该 goroutine
if waitio || netpollcheckerr(pd, mode) == 0 {
// 注册 netpollblockcommit 回调给 gopark,在 gopark 内部会执行它,保存当前 goroutine 到 gpp
gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceEvGoBlockNet, 5)
}
// be careful to not lose concurrent READY notification
old := atomic.Xchguintptr(gpp, 0)
if old > pdWait {
throw("runtime: corrupted polldesc")
}
return old == pdReady
}

// gopark 会停住当前的 goroutine 并且调用传递进来的回调函数 unlockf,从上面的源码我们可以知道这个函数是
// netpollblockcommit
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
if reason != waitReasonSleep {
checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
}
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
if status != _Grunning && status != _Gscanrunning {
throw("gopark: bad g status")
}
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason
mp.waittraceev = traceEv
mp.waittraceskip = traceskip
releasem(mp)
// can't do anything that might move the G between Ms here.
// gopark 最终会调用 park_m,在这个函数内部会调用 unlockf,也就是 netpollblockcommit,
// 然后会把当前的 goroutine,也就是 g 数据结构保存到 pollDesc 的 rg 或者 wg 指针里
mcall(park_m)
}

// park continuation on g0.
func park_m(gp *g) {
_g_ := getg()

if trace.enabled {
traceGoPark(_g_.m.waittraceev, _g_.m.waittraceskip)
}

casgstatus(gp, _Grunning, _Gwaiting)
dropg()

if fn := _g_.m.waitunlockf; fn != nil {
// 调用 netpollblockcommit,把当前的 goroutine,
// 也就是 g 数据结构保存到 pollDesc 的 rg 或者 wg 指针里
ok := fn(gp, _g_.m.waitlock)
_g_.m.waitunlockf = nil
_g_.m.waitlock = nil
if !ok {
if trace.enabled {
traceGoUnpark(gp, 2)
}
casgstatus(gp, _Gwaiting, _Grunnable)
execute(gp, true) // Schedule it back, never returns.
}
}
schedule()
}

// netpollblockcommit 在 gopark 函数里被调用
func netpollblockcommit(gp *g, gpp unsafe.Pointer) bool {
// 通过原子操作把当前 goroutine 抽象的数据结构 g,也就是这里的参数 gp 存入 gpp 指针,
// 此时 gpp 的值是 pollDesc 的 rg 或者 wg 指针
r := atomic.Casuintptr((*uintptr)(gpp), pdWait, uintptr(unsafe.Pointer(gp)))
if r {
// Bump the count of goroutines waiting for the poller.
// The scheduler uses this to decide whether to block
// waiting for the poller if there is nothing else to do.
atomic.Xadd(&netpollWaiters, 1)
}
return r
}

netpoll

前面已经从源码的角度分析完了 netpoll 是如何通过 park goroutine 从而达到阻塞 Accept/Read/Write 的效果,而通过调用 gopark,goroutine 会被放置在某个等待队列中(如 channel 的 waitq ,此时 G 的状态由_Grunning_Gwaitting),因此G必须被手动唤醒(通过 goready ),否则会丢失任务,应用层阻塞通常使用这种方式。

所以,最后还有一个非常关键的问题是:当 I/O 事件发生之后,netpoll 是通过什么方式唤醒那些在 I/O wait 的 goroutine 的?答案是通过 epoll_wait,在 Go 源码中的 src/runtime/netpoll_epoll.go文件中有一个 func netpoll(block bool) gList 方法,它会内部调用epoll_wait获取就绪的 fd 列表,并将每个 fd 对应的 goroutine 添加到链表返回

// polls for ready network connections
// returns list of goroutines that become runnable
func netpoll(block bool) gList {
if epfd == -1 {
return gList{}
}
waitms := int32(-1)
// 是否以阻塞模式调用 epoll_wait
if !block {
waitms = 0
}
var events [128]epollevent
retry:
// 获取就绪的 fd 列表
n := epollwait(epfd, &events[0], int32(len(events)), waitms)
if n < 0 {
if n != -_EINTR {
println("runtime: epollwait on fd", epfd, "failed with", -n)
throw("runtime: netpoll failed")
}
goto retry
}
// toRun 是一个 g 的链表,存储要恢复的 goroutines,最后返回给调用方
var toRun gList
for i := int32(0); i < n; i++ {
ev := &events[i]
if ev.events == 0 {
continue
}
var mode int32
// 判断发生的事件类型,读类型或者写类型
if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {
mode += 'r'
}
if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {
mode += 'w'
}
if mode != 0 {
// 取出保存在 epollevent 里的 pollDesc
pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
pd.everr = false
if ev.events == _EPOLLERR {
pd.everr = true
}
// 调用 netpollready,传入就绪 fd 的 pollDesc,把 fd 对应的 goroutine 添加到链表 toRun 中
netpollready(&toRun, pd, mode)
}
}
if block && toRun.empty() {
goto retry
}
return toRun
}

// netpollready 调用 netpollunblock 返回就绪 fd 对应的 goroutine 的抽象数据结构 g
func netpollready(toRun *gList, pd *pollDesc, mode int32) {
var rg, wg *g
if mode == 'r' || mode == 'r'+'w' {
rg = netpollunblock(pd, 'r', true)
}
if mode == 'w' || mode == 'r'+'w' {
wg = netpollunblock(pd, 'w', true)
}
if rg != nil {
toRun.push(rg)
}
if wg != nil {
toRun.push(wg)
}
}

// netpollunblock 会依据传入的 mode 决定从 pollDesc 的 rg 或者 wg 取出当时 gopark 之时存入的
// goroutine 抽象数据结构 g 并返回
func netpollunblock(pd *pollDesc, mode int32, ioready bool) *g {
// mode == 'r' 代表当时 gopark 是为了等待读事件,而 mode == 'w' 则代表是等待写事件
gpp := &pd.rg
if mode == 'w' {
gpp = &pd.wg
}

for {
// 取出 gpp 存储的 g
old := *gpp
if old == pdReady {
return nil
}
if old == 0 && !ioready {
// Only set READY for ioready. runtime_pollWait
// will check for timeout/cancel before waiting.
return nil
}
var new uintptr
if ioready {
new = pdReady
}
// 重置 pollDesc 的 rg 或者 wg
if atomic.Casuintpt

[ gev ] Go 语言优雅处理 TCP “粘包”

文章分享惜朝 发表了文章 • 0 个评论 • 620 次浏览 • 2019-11-01 10:48 • 来自相关话题

https://github.com/Allenxuxu/gev ...查看全部

https://github.com/Allenxuxu/gev

gev 是一个轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库,支持自定义协议,轻松快速搭建高性能服务器。

TCP 为什么会粘包


TCP 本身就是面向流的协议,就是一串没有界限的数据。所以本质上来说 TCP 粘包是一个伪命题。

TCP 底层并不关心上层业务数据,会套接字缓冲区的实际情况进行包的划分,一个完整的业务数据可能会被拆分成多次进行发送,也可能会将多个小的业务数据封装成一个大的数据包发送(Nagle算法)。

gev 如何优雅处理


gev 通过回调函数 OnMessage 通知用户数据到来,回调函数中会将用户数据缓冲区(ringbuffer)通过参数传递过来。

用户通过对 ringbuffer 操作,来进行数据解包,获取到完整用户数据后再进行业务操作。这样又一个明显的缺点,就是会让业务操作和自定义协议解析代码堆在一起。

所以,最近对 gev 进行了一次较大改动,主要是为了能够以插件的形式支持各种自定义的数据协议,让使用者可以便捷处理 TCP 粘包问题,专注于业务逻辑。



做法如下,定义一个接口 Protocol

```go
// Protocol 自定义协议编解码接口
type Protocol interface {
UnPacket(c *Connection, buffer *ringbuffer.RingBuffer) (interface{}, []byte)
Packet(c *Connection, data []byte) []byte
}
```


用户只需实现这个接口,并注册到 server 中,当客户端数据到来时,gev 会首先调用 UnPacket 方法,如果缓冲区中的数据足够组成一帧,则将数据解包,并返回真正的用户数据,然后在回调 OnMessage 函数并将数据通过参数传递。

下面,我们实现一个简单的自定义协议插件,来启动一个 Server :

```text
| 数据长度 n | payload |
| 4字节 | n 字节 |
```

```go
// protocol.go
package main

import (
"encoding/binary"
"github.com/Allenxuxu/gev/connection"
"github.com/Allenxuxu/ringbuffer"
"github.com/gobwas/pool/pbytes"
)

const exampleHeaderLen = 4

type ExampleProtocol struct{}

func (d *ExampleProtocol) UnPacket(c *connection.Connection, buffer *ringbuffer.RingBuffer) (interface{}, []byte) {
if buffer.VirtualLength() > exampleHeaderLen {
buf := pbytes.GetLen(exampleHeaderLen)
defer pbytes.Put(buf)
_, _ = buffer.VirtualRead(buf)
dataLen := binary.BigEndian.Uint32(buf)

if buffer.VirtualLength() >= int(dataLen) {
ret := make([]byte, dataLen)
_, _ = buffer.VirtualRead(ret)

buffer.VirtualFlush()
return nil, ret
} else {
buffer.VirtualRevert()
}
}
return nil, nil
}

func (d *ExampleProtocol) Packet(c *connection.Connection, data []byte) []byte {
dataLen := len(data)
ret := make([]byte, exampleHeaderLen+dataLen)
binary.BigEndian.PutUint32(ret, uint32(dataLen))
copy(ret[4:], data)
return ret
}
```

```go
// server.go
package main

import (
"flag"
"log"
"strconv"

"github.com/Allenxuxu/gev"
"github.com/Allenxuxu/gev/connection"
)

type example struct{}

func (s *example) OnConnect(c *connection.Connection) {
log.Println(" OnConnect : ", c.PeerAddr())
}
func (s *example) OnMessage(c *connection.Connection, ctx interface{}, data []byte) (out []byte) {
log.Println("OnMessage:", data)
out = data
return
}

func (s *example) OnClose(c *connection.Connection) {
log.Println("OnClose")
}

func main() {
handler := new(example)
var port int
var loops int

flag.IntVar(&port, "port", 1833, "server port")
flag.IntVar(&loops, "loops", -1, "num loops")
flag.Parse()

s, err := gev.NewServer(handler,
gev.Address(":"+strconv.Itoa(port)),
gev.NumLoops(loops),
gev.Protocol(&ExampleProtocol{}))
if err != nil {
panic(err)
}

log.Println("server start")
s.Start()
}
```
完整代码地址

当回调 `OnMessage` 函数的时候,会通过参数传递已经拆好包的用户数据。

当我们需要使用其他协议时,仅仅需要实现一个 Protocol 插件,然后只要 `gev.NewServer` 时指定即可:

```go
gev.NewServer(handler, gev.NumLoops(2), gev.Protocol(&XXXProtocol{}))
```

## 基于 Protocol Plugins 模式为 gev 实现 WebSocket 插件

得益于 Protocol Plugins 模式的引进,我可以将 WebSocket 的实现做成一个插件(WebSocket 协议构建在 TCP 之上),独立于 gev 之外。

```go
package websocket

import (
"log"

"github.com/Allenxuxu/gev/connection"
"github.com/Allenxuxu/gev/plugins/websocket/ws"
"github.com/Allenxuxu/ringbuffer"
)

// Protocol websocket
type Protocol struct {
upgrade *ws.Upgrader
}

// New 创建 websocket Protocol
func New(u *ws.Upgrader) *Protocol {
return &Protocol{upgrade: u}
}

// UnPacket 解析 websocket 协议,返回 header ,payload
func (p *Protocol) UnPacket(c *connection.Connection, buffer *ringbuffer.RingBuffer) (ctx interface{}, out []byte) {
upgraded := c.Context()
if upgraded == nil {
var err error
out, _, err = p.upgrade.Upgrade(buffer)
if err != nil {
log.Println("Websocket Upgrade :", err)
return
}
c.SetContext(true)
} else {
header, err := ws.VirtualReadHeader(buffer)
if err != nil {
log.Println(err)
return
}
if buffer.VirtualLength() >= int(header.Length) {
buffer.VirtualFlush()

payload := make([]byte, int(header.Length))
_, _ = buffer.Read(payload)

if header.Masked {
ws.Cipher(payload, header.Mask, 0)
}

ctx = &header
out = payload
} else {
buffer.VirtualRevert()
}
}
return
}

// Packet 直接返回
func (p *Protocol) Packet(c *connection.Connection, data []byte) []byte {
return data
}
```

具体的实现,可以到仓库的 [plugins/websocket](https://github.com/Allenxuxu/gev/tree/master/plugins/websocket) 查看。

## 相关文章

- [开源 gev: Go 实现基于 Reactor 模式的非阻塞 TCP 网络库](https://note.mogutou.xyz/articles/2019/09/19/1568896693634.html)
- [Go 网络库并发吞吐量测试](https://note.mogutou.xyz/articles/2019/09/22/1569146969662.html)

## 项目地址

https://github.com/Allenxuxu/gev

[开源] gev (支持 websocket 啦): Go 实现基于 Reactor 模式的非阻塞网络库

开源程序惜朝 发表了文章 • 0 个评论 • 352 次浏览 • 2019-10-24 11:04 • 来自相关话题

https://github.com/Allenxuxu/gev[gev](https://github.com/Allenxuxu/gev) ...查看全部

https://github.com/Allenxuxu/gev

[gev](https://github.com/Allenxuxu/gev) 是一个轻量、快速、高性能的基于 Reactor 模式的非阻塞网络库,底层并不使用 golang net 库,而是使用 epoll 和 kqueue。

现在它支持 WebSocket 啦!

支持定时任务,延时任务!

⬇️⬇️⬇️

## 特点

- 基于 epoll 和 kqueue 实现的高性能事件循环
- 支持多核多线程
- 动态扩容 Ring Buffer 实现的读写缓冲区
- 异步读写
- SO_REUSEPORT 端口重用支持
- 支持 WebSocket
- 支持定时任务,延时任务

## 性能测试

> 测试环境 Ubuntu18.04
- gev
- gnet
- eviop
- evio
- net (标准库)

### 吞吐量测试




仓库地址: https://github.com/Allenxuxu/gev

[gev] 一个轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库

Go开源项目惜朝 发表了文章 • 0 个评论 • 356 次浏览 • 2019-09-25 16:45 • 来自相关话题

`gev` 是一个 ...查看全部

`gev` 是一个轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库。

➡️➡️ https://github.com/Allenxuxu/gev

特点

- 基于 epoll 和 kqueue 实现的高性能事件循环
- 支持多核多线程
- 动态扩容 Ring Buffer 实现的读写缓冲区
- 异步读写
- SO_REUSEPORT 端口重用支持

网络模型

`gev` 只使用极少的 goroutine, 一个 goroutine 负责监听客户端连接,其他 goroutine (work 协程)负责处理已连接客户端的读写事件,work 协程数量可以配置,默认与运行主机 CPU 数量相同。

性能测试

> 测试环境 Ubuntu18.04 | 4 Virtual CPUs | 4.0 GiB

吞吐量测试

限制 GOMAXPROCS=1(单线程),1 个 work 协程


限制 GOMAXPROCS=4,4 个 work 协程


其他测试

速度测试


和同类库的简单性能比较, 压测方式与 evio 项目相同。
- gnet
- eviop
- evio
- net (标准库)

限制 GOMAXPROCS=1,1 个 work 协程


限制 GOMAXPROCS=1,4 个 work 协程


限制 GOMAXPROCS=4,4 个 work 协程


安装 gev


```bash
go get -u github.com/Allenxuxu/gev
```

快速入门


```go
package main

import (
"log"

"github.com/Allenxuxu/gev"
"github.com/Allenxuxu/gev/connection"
"github.com/Allenxuxu/ringbuffer"
)

type example struct{}

func (s *example) OnConnect(c *connection.Connection) {
log.Println(" OnConnect : ", c.PeerAddr())
}

func (s *example) OnMessage(c *connection.Connection, buffer *ringbuffer.RingBuffer) (out []byte) {
log.Println("OnMessage")
first, end := buffer.PeekAll()
out = first
if len(end) > 0 {
out = append(out, end...)
}
buffer.RetrieveAll()
return
}

func (s *example) OnClose(c *connection.Connection) {
log.Println("OnClose")
}

func main() {
handler := new(example)

s, err := gev.NewServer(handler,
gev.Address(":1833"),
gev.NumLoops(2),
gev.ReusePort(true))
if err != nil {
panic(err)
}

s.Start()
}
```

Handler 是一个接口,我们的程序必须实现它。

```go
type Handler interface {
OnConnect(c *connection.Connection)
OnMessage(c *connection.Connection, buffer *ringbuffer.RingBuffer) []byte
OnClose(c *connection.Connection)
}

func NewServer(handler Handler, opts ...Option) (server *Server, err error) {
```

在消息到来时,gev 会回调 OnMessage ,在这个函数中可以通过返回一个切片来发送数据给客户端。

```go
func (s *example) OnMessage(c *connection.Connection, buffer *ringbuffer.RingBuffer) (out []byte)
```

Connection 还提供 Send 方法来发送数据。Send 并不会立刻发送数据,而是先添加到 event loop 的任务队列中,然后唤醒 event loop 去发送。

更详细的使用方式可以参考示例:[服务端定时推送]

```go
func (c *Connection) Send(buffer []byte) error
```

Connection ShutdownWrite 会关闭写端,从而断开连接。

更详细的使用方式可以参考示例:[限制最大连接数]

```go
func (c *Connection) ShutdownWrite() error
```


➡️➡️ https://github.com/Allenxuxu/gev



微软、IBM、GitLab 等大厂全部到齐的 OCS 第一天有什么看点?

文章分享NebulaGraph 发表了文章 • 0 个评论 • 739 次浏览 • 2019-09-19 11:24 • 来自相关话题

在本周一的[推文](https://mp.weixin.qq.com/s/vLnfwiqgPlhvf_O7ixPQTg)中我们大致介绍了下 Open Core 峰会及到场嘉宾,(≧▽≦) 当然还有 Nebula Graph 在会场的展位位置图,本文我们来看看 ...查看全部
在本周一的[推文](https://mp.weixin.qq.com/s/vLnfwiqgPlhvf_O7ixPQTg)中我们大致介绍了下 Open Core 峰会及到场嘉宾,(≧▽≦) 当然还有 Nebula Graph 在会场的展位位置图,本文我们来看看 Open Core 峰会第一天有哪些值得一看的议题。

本文目录

- Adventures and Misadventures in Category Creation & OSS: The Neo4j Story - Emil Eifrem, Neo4j, Inc.
- [Creating Authentic Value: Open Source vs. Open Core - Deborah Bryant, Red Hat, Inc.](https://opencoresummit2019.sched.com/event/UNK9/creating-authentic-value-open-source-vs-open-core-deborah-bryant-red-hat-inc)
- [Commercial Open Source Business Models - In the Age of Hyper-Clouds, GitLab bets on Buyer-based Open Core - Priyanka Sharma, GitLab Inc.](https://opencoresummit2019.sched.com/event/UNKC/commercial-open-source-business-models-in-the-age-of-hyper-clouds-gitlab-bets-on-buyer-based-open-core-priyanka-sharma-gitlab-inc)
- [Opening up the cloud with Crossplane - Bassam Tabbara, Upbound](https://opencoresummit2019.sched.com/event/UNKF/opening-up-the-cloud-with-crossplane-bassam-tabbara-upbound)
- [Decentralization: A new opportunity for open source monetization - Ben Golub, Storj Labs](https://opencoresummit2019.sched.com/event/UNKI/decentralization-a-new-opportunity-for-open-source-monetization-ben-golub-storj-labs)
- [Open Source Adoption: The Pitfalls and Victories - Ido Green, JFrog](https://opencoresummit2019.sched.com/event/UNKO/open-source-adoption-the-pitfalls-and-victories-ido-green-jfrog)
- [On building a business around viable open-source project - Kohsuke Kawaguchi, CloudBees, Inc.Kohsuke Kawaguchi](https://opencoresummit2019.sched.com/event/UNKU/on-building-a-business-around-viable-open-source-project-kohsuke-kawaguchi-cloudbees-inc)
- [Your Product and Your Project are Different - Sarah Novotny, Microsoft](https://opencoresummit2019.sched.com/event/UNKX/your-product-and-your-project-are-different-sarah-novotny-microsoft)
- The open source journey from Initical code to IPO - Shay Banon, Elasticsearch creator/founder
- [PANEL - Investing in Open Source - From Promising Project to Enduring Company - Konstantine Buhler, Meritech Capital (Moderator)](https://opencoresummit2019.sched.com/event/UNKm/panel-investing-in-open-source-from-promising-project-to-enduring-company-konstantine-buhler-meritech-capital-moderator)
- [Percona - Monetizing Open Source without Open Core - Peter Zaitsev, Percona](https://opencoresummit2019.sched.com/event/UNKs/percona-monetizing-open-source-without-open-core-peter-zaitsev-percona)
- [Cygnus: COSS From the Absolute Beginning - David Henkel-Wallace, Leela.ai](https://opencoresummit2019.sched.com/event/UNKv/cygnus-coss-from-the-absolute-beginning-david-henkel-wallace-leelaai)
- [Software-Defined Telecom: From Open Source to Mainstream, Bringing Complex Technology to the Masses - Anthony Minessale, SignalWire INC](https://opencoresummit2019.sched.com/event/UNKy/software-defined-telecom-from-open-source-to-mainstream-bringing-complex-technology-to-the-masses-anthony-minessale-signalwire-inc)
- [Open Source and Open Core – Not a Zero Sum Game - Andi Gutmans, AWS](https://opencoresummit2019.sched.com/event/UNL1/open-source-and-open-core-not-a-zero-sum-game-andi-gutmans-aws)
- [Dual licensing: its place in an Open Source business, with reflections on a 32-year success story - L Peter Deutsch, Artifex Software](https://opencoresummit2019.sched.com/event/UNL4/dual-licensing-its-place-in-an-open-source-business-with-reflections-on-a-32-year-success-story-l-peter-deutsch-artifex-software)
- [Disrupting the Enterprise with Open Source - Marco Palladino, Kong](https://opencoresummit2019.sched.com/event/UNL7/disrupting-the-enterprise-with-open-source-marco-palladino-kong)
- [The New Business Model - Creating Operational Excellence around Open Source with Cloud - Jason McGee, IBM](https://opencoresummit2019.sched.com/event/UNLA/the-new-business-model-creating-operational-excellence-around-open-source-with-cloud-jason-mcgee-ibm)
- [Going Global with Open Core - John Newton, Alfresco Software](https://opencoresummit2019.sched.com/event/UNLD/going-global-with-open-core-john-newton-alfresco-software)
- [Making Money with Open Source - Marten Mickos, HackerOne](https://opencoresummit2019.sched.com/event/UkwM/making-money-with-open-source-marten-mickos-hackerone)

![](https://oscimg.oschina.net/oscnet/fc09fe47f578400bb0ebde6ff5c4da36f0f.jpg)

### [Adventures and Misadventures in Category Creation & OSS: The Neo4j Story - Emil Eifrem, Neo4j, Inc.](https://opencoresummit2019.sched.com/event/UNK6/adventures-and-misadventures-in-category-creation-oss-the-neo4j-story-emil-eifrem-neo4j-inc)

Neo4j Founder and CEO Emil Eifrem will share war stories from a journey that began with sketching out the property graph data model on a napkin, and has led to running the leading company in the fastest growing database category.

All the while trying (and sometimes succeeding!) to combine category creation, open source, developer marketing and enterprise selling into a complex brew that will one day lead to inevitable world domination.

> 不知道作为现图数据库领域的领头羊,Neo4j 除了给我们科普构建图模型之外,会给我们带来怎么样的惊喜…

![](https://oscimg.oschina.net/oscnet/1f0360039496d902ae4b44d2e2a5a91dca5.jpg)

### [Creating Authentic Value: Open Source vs. Open Core - Deborah Bryant, Red Hat, Inc.](https://opencoresummit2019.sched.com/event/UNK9/creating-authentic-value-open-source-vs-open-core-deborah-bryant-red-hat-inc)

Recent emphasis on cloud technologies has put a spotlight on how software companies work in today’s business and technical environments. Some companies have moved from traditional software licenses and instead have chosen to try to protect their software through creative licenses such as “open core”. Unlike open source, where value is placed on community, collaboration, and services, open core businesses place their value on software features.

Red Hat’s successful experience as a completely open source company has shown that value is not in the code, but in the support and expertise by being a part of a true community. In this talk, Red Hat’s Deb Bryant will share observations and cautionary tales from the world’s most successful open source company on how open core has time and again been demonstrated to not be truly open, limits community innovation, and delivers essentially proprietary software to customers.

> Red Hat 本次带来的演讲主要是和开源许可证有关,不禁让人想起去年 Redis 和 Neo4j 都更改了开源许可证的事情,希望本次 Red Hat 的分享能解答我们对开放源码和开放软件核心业务的部分疑惑…

![](https://oscimg.oschina.net/oscnet/a3e9635a14d4ac173300e880b369d87836a.jpg)

### [Commercial Open Source Business Models - In the Age of Hyper-Clouds, GitLab bets on Buyer-based Open Core - Priyanka Sharma, GitLab Inc.](https://opencoresummit2019.sched.com/event/UNKC/commercial-open-source-business-models-in-the-age-of-hyper-clouds-gitlab-bets-on-buyer-based-open-core-priyanka-sharma-gitlab-inc)

Today is the day of hyper clouds and companies based on open source projects have challenges building a business. GitLab, the first single application for the DevSecOps lifecycle, has grown 177% YoY at the same time. In this talk, Priyanka Sharma, Director of Technical Evangelism, GitLab Inc. will share the road the company took to success. She will talk about:

- Implications of being a commercial open source (COSS) company today
- The business models GitLab considered
- Our chosen model of buyer based open core
- How buyer based open core works
- Advantages of our choice

This talk is ideal for anyone looking to understand how open source can be monetized and will provide unique insight whether you are a startup founder, end user, or cloud provider.

> GitLab 的演讲主要是如何将开源和商业化相结合,开源本身并不是一种商业模式,而是一种开发模式和软件的推广,或者说是传播模式,GitLab 本次的演讲也许能给我们一些开源同商业相结合的新启发。

![](https://oscimg.oschina.net/oscnet/a6f7efdd4c8ca056c8bc573490bb0bd8944.jpg)

### [Opening up the cloud with Crossplane - Bassam Tabbara, Upbound](https://opencoresummit2019.sched.com/event/UNKF/opening-up-the-cloud-with-crossplane-bassam-tabbara-upbound)

Today, cloud computing is dominated by a few vertically integrated commercial providers. In this talk Bassam Tabbara, founder of the open source Crossplane project and CEO of Upbound.io, will discuss how the open source community can tip the market towards a more open, horizontally-integrated cloud ecosystem.

![](https://oscimg.oschina.net/oscnet/b09dd636e0ca057d62d474e8d99c9e8df1e.jpg)

### [Decentralization: A new opportunity for open source monetization - Ben Golub, Storj Labs](https://opencoresummit2019.sched.com/event/UNKI/decentralization-a-new-opportunity-for-open-source-monetization-ben-golub-storj-labs)

The emergence of cloud computing has spurred massive innovation, decoupling software from the infrastructure on which it runs. However it has also brought about huge challenges for open source companies in search of sustainable business models, causing them incorporate new licensing models other tactics to compete against the cloud computing giants. Decentralized cloud platforms make new economic models possible that allow open source platforms to monetize all their customers - not just their enterprise users.

This session will look at the intersection of open source, decentralization, and cloud and how it can empower open source platforms in new ways. It will also explore how decentralization and open source software can combine to eliminate downtime in the cloud, remove single points of failure, democratize trust, improve accountability, and radically improve security and privacy at a foundational level.

> 云计算将软件同其运行环境相分离,Storj Las 带来的演讲侧重点在于开放源码、分散化和云的相结合实践方案,此外你对云计算中的安全、隐私方面有兴趣的,这个主题或许能给你带来新的领悟。

![](https://oscimg.oschina.net/oscnet/315c8b6844089cb88c1a239c595b3cb1710.jpg)

### [Open Source Adoption: The Pitfalls and Victories - Ido Green, JFrog](https://opencoresummit2019.sched.com/event/UNKO/open-source-adoption-the-pitfalls-and-victories-ido-green-jfrog)

In this talk, we’ll share some key insights so other project owners can avoid falling into the same holes we’ve fallen into. Further, we’ll share some interesting statistics about the DevOps market that will help you gain insight into your own domain, and how you can practically address larger market movements that the bosses’ bosses’ bosses are really caring about.

> 看样子,JFrog 这次的会给我们带来大量的 DevOps 数据方面的分享,不知他们在市场上踩过的坑能给我们带来多少启发:)

![](https://oscimg.oschina.net/oscnet/a45dc8e270a591ad5292060ec2cc62e68bd.jpg)

### [On building a business around viable open-source project - Kohsuke Kawaguchi, CloudBees, Inc.Kohsuke Kawaguchi](https://opencoresummit2019.sched.com/event/UNKU/on-building-a-business-around-viable-open-source-project-kohsuke-kawaguchi-cloudbees-inc)

In this talk, I’d like to look back at the history and share what worked and what didn’t work, such as the difficulty of justifying engineering efforts to OSS, how enterprise product can stifle open-source, and the impact and the consequences of hiring people from the comm

> CloudBees 的演讲摘要中规中矩,不知周四的现场演讲会不会带来不一样的体验…

![](https://oscimg.oschina.net/oscnet/bbc3b25cc874adb838982f2e432cbd5ce49.jpg)

### [Your Product and Your Project are Different - Sarah Novotny, Microsoft](https://opencoresummit2019.sched.com/event/UNKX/your-product-and-your-project-are-different-sarah-novotny-microsoft)

Open source is a licensing and development model. Your open source project may be the basis for your product, but they are two different things. Let’s talk about a few challenges which can happen if that distinction is forgotten.

![](https://oscimg.oschina.net/oscnet/15a05aba1d16cc033cc24eefc22e6a35fa4.jpg)

### [The open source journey from Initical code to IPO - Shay Banon, Elasticsearch creator/founder](https://opencoresummit2019.sched.com/event/UNKa)

Elasticsearch creator/founder and Elastic CEO shares first-hand experiences writing the first million lines of code (likely more!) and guiding the company to an IPO eight years later.

![](https://oscimg.oschina.net/oscnet/dc0fbc0f35641e8a4c36700694de24523ae.jpg)

### [PANEL - Investing in Open Source - From Promising Project to Enduring Company - Konstantine Buhler, Meritech Capital (Moderator)](https://opencoresummit2019.sched.com/event/UNKm/panel-investing-in-open-source-from-promising-project-to-enduring-company-konstantine-buhler-meritech-capital-moderator)

Join two of the world's top open source investors, Martin Casado (Andreessen Horowitz) and Mike Volpi (Index Ventures) as they discuss open source investing. Every open source company begins with a passionate community. But the most enduring open source companies navigate a nuanced path to commercialization. Don't miss out as Martin and Mike share stories, discuss patterns, and offer insight into building incredible open source companies. The panel will be moderated by Konstantine Buhler (Meritech Capital).

> "每个开源公司都始于一个充满激情的社区", 也许 Martin 和 Mike 的故事能给做开源社区的公司指一条明路…

![](https://oscimg.oschina.net/oscnet/59e02b297571758454485715ea1692c0253.jpg)

### [Percona - Monetizing Open Source without Open Core - Peter Zaitsev, Percona](https://opencoresummit2019.sched.com/event/UNKs/percona-monetizing-open-source-without-open-core-peter-zaitsev-percona)

All Percona Software Products are 100% Free and Open Source. We do not do Open Core, Shared Source or Open Source Eventually. Yet Percona has been growing every one of 13 years it has been in existence, all without relying on Venture Capital or other External Funding, and now reaching over $20M of ARR. In this presentation you will hear our story and why our approach to business may (or may not) workfor you.

> 开源数据库技术大会主办方 Percona 好像和 Open Core 峰会唱了个反调:without Open Core :) ,也许我们也能学习下如何通过开源和软件,且不靠融资,获得 2 千美元的 ARR

![](https://oscimg.oschina.net/oscnet/08db7c419f5c3238ee24bf669040df0d3bf.jpg)

### [Cygnus: COSS From the Absolute Beginning - David Henkel-Wallace, Leela.ai](https://opencoresummit2019.sched.com/event/UNKv/cygnus-coss-from-the-absolute-beginning-david-henkel-wallace-leelaai)

In this talk I will briefly describe the software ecosystem of 1989 and what inspired me to found Cygnus, along with Michael Tiemann and John Gilmore, in my living room. I will then discuss what has changed and what has remained the same since then. Finally, I will address some risks I see for the COSS ecosystem, based on.

![](https://oscimg.oschina.net/oscnet/a4b0417ae1f012c589f13cbe55b0dd5888a.jpg)

### [Software-Defined Telecom: From Open Source to Mainstream, Bringing Complex Technology to the Masses - Anthony Minessale, SignalWire INC](https://opencoresummit2019.sched.com/event/UNKy/software-defined-telecom-from-open-source-to-mainstream-bringing-complex-technology-to-the-masses-anthony-minessale-signalwire-inc)
From the early days of VoIP starting with Asterisk and through the advent of FreeSWITCH and on to WebRTC, Learn how the evolution of Software-Defined Telecom led by SignalWire has brought us to a new era in telecommunications.
> 看样子能 Get 新电信时代不少信息,不知道会给通信这块业务带来怎么样的灵感。

![](https://oscimg.oschina.net/oscnet/944a077fc133cf95b63f23e84bdfdb83712.jpg)

### [Open Source and Open Core – Not a Zero Sum Game - Andi Gutmans, AWS](https://opencoresummit2019.sched.com/event/UNL1/open-source-and-open-core-not-a-zero-sum-game-andi-gutmans-aws)
Gutmans will talk about his early journey in open source, how he’s seen open source evolve over the years, and why the relation between open source and open core is not a zero sum game.

![](https://oscimg.oschina.net/oscnet/1ab786b1baae900c9c1833d2049ac575804.jpg)

### [Dual licensing: its place in an Open Source business, with reflections on a 32-year success story - L Peter Deutsch, Artifex Software](https://opencoresummit2019.sched.com/event/UNL4/dual-licensing-its-place-in-an-open-source-business-with-reflections-on-a-32-year-success-story-l-peter-deutsch-artifex-software)

This talk will cover one of many possible maps of how to structure thinking about Open Source business; how Ghostscript does and doesn't align with that map; what has made Ghostscript successful; and a perspective on dual licensing in general.

![](https://oscimg.oschina.net/oscnet/63e9b4b635a5231e22f8fd021f42556605b.jpg)

### [Disrupting the Enterprise with Open Source - Marco Palladino, Kong](https://opencoresummit2019.sched.com/event/UNL7/disrupting-the-enterprise-with-open-source-marco-palladino-kong)

The Open Source revolution transforms and scales the modern Enterprise by increasing team productivity, and improving business scalability. Specifically when it comes to networking and services, OSS technologies have played an important role in redefining entire applications and architectures, ultimately transforming the organization in a distributed organism. This session explores the open source revolution, and its impacts in the Enterprise, and how it transformed organizations operationally, technologically and culturally.

> 看,又一个开源项目实践分享,不知道有什么新切入点

![](https://oscimg.oschina.net/oscnet/516aeeb52acc28a5419056d45e5c66f2f5b.jpg)

### [The New Business Model - Creating Operational Excellence around Open Source with Cloud - Jason McGee, IBM](https://opencoresummit2019.sched.com/event/UNLA/the-new-business-model-creating-operational-excellence-around-open-source-with-cloud-jason-mcgee-ibm)

Public clouds have risen with an interesting mix of Open Source and Proprietary software and APIs. At IBM, we have built our global public cloud on an open source foundation. In this talk Jason will discuss the value that can be created delivering open source technologies as a service on cloud, including the role Open Source plays in cloud, the power of open source in enabling clients to leverage cloud in a portable way, the incredible combination of open source projects that had to come together to create and operate a full stack cloud platform and the rise of new value and business models for open source that are enabled by as-a-Service cloud delivery.

> IBM 的本次演讲也是围绕着本次大会的热门议题:云计算,包括开源在云中所起的作用、将开源的力量和云移动相结合,最终实现开源云交付服务。

![](https://oscimg.oschina.net/oscnet/caa195d8e54f54cddf09712c5c50d6227c6.jpg)

### [Going Global with Open Core - John Newton, Alfresco Software](https://opencoresummit2019.sched.com/event/UNLD/going-global-with-open-core-john-newton-alfresco-software)

To build a successful open source business, you have to think beyond your project and start thinking globally almost from day one. Taking and contrasting experiences of building both an open source, open core business and a proprietary software business, John presents the advantages and challenges of building a global software business using an open core model. This presentation examines the role of the community, partners, channels and the project in moving beyond a home market.

Some of the issues addressed are: how do you look beyond your home market, how do you hire to grow globally, how does an open core model work in a global market, how do you fund global growth, and how do you compete against giant proprietary incumben.

![](https://oscimg.oschina.net/oscnet/9a0afaa9bafaf0996682332f8006d57d04c.jpg)

### [Making Money with Open Source - Marten Mickos, HackerOne](https://opencoresummit2019.sched.com/event/UkwM/making-money-with-open-source-marten-mickos-hackerone)

As for making money with open source, Marten coined the saying "Some people will spend any amount of time to save money; others will spend money to save time." which is key to figuring out how to make money in open source.

### 图图小感悟

浏览了 Open Core Summit 第一天的演讲摘要,「云计算」和「OSS 技术」是本届峰会的宠儿,由于摘要的内容有限,图图只能和大家小点评了下,欢迎关注 Nebula Graph 的订阅号:Nebula Graph Community 查看会场的演讲 (≧▽≦) 想加入图数据库交流群的请添加微信:NebulaGraphbot

![](https://oscimg.oschina.net/oscnet/acae3c421682228eed4bc46ccaede0efe77.jpg)

> Nebula Graph:一个开源的分布式图数据库。

> GitHub:[https://github.com/vesoft-inc/nebula](https://0x7.me/go2github)

> 知乎:https://www.zhihu.com/org/nebulagraph/posts

> 微博:https://weibo.com/nebulagraph

gnet: 一个轻量级且高性能的 Go 网络库

开源程序panjf2000 发表了文章 • 1 个评论 • 577 次浏览 • 2019-09-18 16:08 • 来自相关话题


gnet












# 博客原文
https://taohuawu.club/go-event-loop-networking-library-gnet

# Github 主页
https://github.com/panjf2000/gnet

欢迎大家围观~~,目前还在持续更新,感兴趣的话可以 star 一下暗中观察哦。

# 简介

`gnet` 是一个基于 Event-Loop 事件驱动的高性能和轻量级网络库。这个库直接使用 [epoll](https://en.wikipedia.org/wiki/Epoll) 和 [kqueue](https://en.wikipedia.org/wiki/Kqueue) 系统调用而非标准 Golang 网络包:[net](https://golang.org/pkg/net/) 来构建网络应用,它的工作原理类似两个开源的网络库:[libuv](https://github.com/libuv/libuv) 和 [libevent](https://github.com/libevent/libevent)。

这个项目存在的价值是提供一个在网络包处理方面能和 [Redis](http://redis.io)、[Haproxy](http://www.haproxy.org) 这两个项目具有相近性能的Go 语言网络服务器框架。

`gnet` 的亮点在于它是一个高性能、轻量级、非阻塞的纯 Go 实现的传输层(TCP/UDP/Unix-Socket)网络库,开发者可以使用 `gnet` 来实现自己的应用层网络协议,从而构建出自己的应用层网络应用:比如在 `gnet` 上实现 HTTP 协议就可以创建出一个 HTTP 服务器 或者 Web 开发框架,实现 Redis 协议就可以创建出自己的 Redis 服务器等等。

**`gnet` 衍生自另一个项目:`evio`,但是性能更好。**

# 功能

- [高性能](#性能测试) 的基于多线程模型的 Event-Loop 事件驱动
- 内置 Round-Robin 轮询负载均衡算法
- 简洁的 APIs
- 基于 Ring-Buffer 的高效内存利用
- 支持多种网络协议:TCP、UDP、Unix Sockets
- 支持两种事件驱动机制:Linux 里的 epoll 以及 FreeBSD 里的 kqueue
- 支持异步写操作
- 允许多个网络监听地址绑定在一个 Event-Loop 上
- 灵活的事件定时器
- SO_REUSEPORT 端口重用

# 核心设计

## 多线程模型

`gnet` 重新设计开发了一个新内置的多线程模型:『主从 Reactor 多线程』,这也是 `netty` 默认的线程模型,下面是这个模型的原理图:


multi_reactor



它的运行流程如下面的时序图:


reactor



现在我正在 `gnet` 里开发一个新的多线程模型:『带线程/go程池的主从 Reactors 多线程』,并且很快就能完成,这个模型的架构图如下所示:


multi_reactor_thread_pool



它的运行流程如下面的时序图:


multi-reactors



## 通信机制

`gnet` 的『主从 Reactors 多线程』模型是基于 Golang 里的 Goroutines的,一个 Reactor 挂载在一个 Goroutine 上,所以在 `gnet` 的这个网络模型里主 Reactor/Goroutine 与从 Reactors/Goroutines 有海量通信的需求,因此 `gnet` 里必须要有一个能在 Goroutines 之间进行高效率的通信的机制,我没有选择 Golang 里的主流方案:基于 Channel 的 CSP 模型,而是选择了性能更好、基于 Ring-Buffer 的 Disruptor 方案。

所以我最终选择了 [go-disruptor](https://github.com/smartystreets-prototypes/go-disruptor):高性能消息分发队列 LMAX Disruptor 的 Golang 实现。

## 自动扩容的 Ring-Buffer

`gnet` 利用 Ring-Buffer 来缓存 TCP 流数据以及管理内存使用。







# 开始使用

## 安装

```sh
$ go get -u github.com/panjf2000/gnet
```

## 使用示例

```go
// ======================== Echo Server implemented with gnet ===========================

package main

import (
"flag"
"fmt"
"log"
"strings"

"github.com/panjf2000/gnet"
"github.com/panjf2000/gnet/ringbuffer"
)

func main() {
var port int
var loops int
var udp bool
var trace bool
var reuseport bool

flag.IntVar(&port, "port", 5000, "server port")
flag.BoolVar(&udp, "udp", false, "listen on udp")
flag.BoolVar(&reuseport, "reuseport", false, "reuseport (SO_REUSEPORT)")
flag.BoolVar(&trace, "trace", false, "print packets to console")
flag.IntVar(&loops, "loops", 0, "num loops")
flag.Parse()

var events gnet.Events
events.NumLoops = loops
events.OnInitComplete = func(srv gnet.Server) (action gnet.Action) {
log.Printf("echo server started on port %d (loops: %d)", port, srv.NumLoops)
if reuseport {
log.Printf("reuseport")
}
return
}
events.React = func(c gnet.Conn, inBuf *ringbuffer.RingBuffer) (out []byte, action gnet.Action) {
top, tail := inBuf.PreReadAll()
out = append(top, tail...)
inBuf.Reset()

if trace {
log.Printf("%s", strings.TrimSpace(string(top)+string(tail)))
}
return
}
scheme := "tcp"
if udp {
scheme = "udp"
}
log.Fatal(gnet.Serve(events, fmt.Sprintf("%s://:%d", scheme, port)))
}

```

## I/O 事件

`gnet` 目前支持的 I/O 事件如下:

- `OnInitComplete` 当 server 初始化完成之后调用。
- `OnOpened` 当连接被打开的时候调用。
- `OnClosed` 当连接被关闭的时候调用。
- `OnDetached` 当主动摘除连接的时候的调用。
- `React` 当 server 端接收到从 client 端发送来的数据的时候调用。(你的核心业务代码一般是写在这个方法里)
- `Tick` 服务器启动的时候会调用一次,之后就以给定的时间间隔定时调用一次,是一个定时器方法。
- `PreWrite` 预先写数据方法,在 server 端写数据回 client 端之前调用。

# 性能测试

## Linux (epoll)

### 系统参数

```powershell
Go Version: go1.12.9 linux/amd64
OS: Ubuntu 18.04
CPU: 8 Virtual CPUs
Memory: 16.0 GiB
```

### Echo Server

![echolinux.png](https://img.hacpai.com/file/2019/09/echolinux-fca6e6e5.png)


### HTTP Server

![httplinux.png](https://img.hacpai.com/file/2019/09/httplinux-663a0318.png)


## FreeBSD (kqueue)

### 系统参数

```powershell
Go Version: go version go1.12.9 darwin/amd64
OS: macOS Mojave 10.14.6
CPU: 4 CPUs
Memory: 8.0 GiB
```

### Echo Server

![echomac.png](https://img.hacpai.com/file/2019/09/echomac-7a29e0d1.png)


### HTTP Server

![httpmac.png](https://img.hacpai.com/file/2019/09/httpmac-cb6d26ea.png)


# 证书

`gnet` 的源码允许用户在遵循 MIT [开源证书](https://github.com/panjf2000/gnet/blob/master/LICENSE) 规则的前提下使用。

# 待做事项

> gnet 还在持续开发的过程中,所以这个仓库的代码和文档会一直持续更新,如果你对 gnet 感兴趣的话,欢迎给这个开源库贡献你的代码~~

图数据库爱好者的聚会在谈论什么?

文章分享NebulaGraph 发表了文章 • 0 个评论 • 681 次浏览 • 2019-09-16 12:23 • 来自相关话题

> [Nebula Graph](https://0x7.me/go2github):一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可 ...查看全部
> [Nebula Graph](https://0x7.me/go2github):一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可用且保障数据安全性。

### 聚会概述

在上周六的聚会中,Nebula Graph Committer 吴敏给爱好者们介绍了整体架构和特性,并随后被各位大佬~~轮番蹂躏~~(划掉)。


本次分享主要介绍了 Nebula Graph 的特性,以及新上线的[《使用 Docker 构建 Nebula Graph》](https://zhuanlan.zhihu.com/p/81316517)功能。

下面是现场的 Topic ( 以下简称:T ) & Discussion ( 以下简称:D ) 速记:

### 讨论话题目录

- 算法和语言
- 图库的 builtin 只搞在线查询可以吗?有必要搞传播算法和最短路径吗?Nebula 怎么实现对图分析算法的支持?
- 为什么要新开发一种查询语言 nGQL?做了哪些优化?
- 对于超大点,有啥优化的办法吗,或者对于构图有什么建议嘛?
- 图库相比其它系统和数据库未来发展趋势,比如相比文档和关系型,它的核心价值是什么?
- 架构和工程
- key 为什么选择用 hash 而不是 range?
- gRPC,bRPC,fbthrift 为什么这么选 rpc?有没有打算自己写一个?
- 图库在设计上趋同化和同质化,架构上还有哪些创新值得尝试?
- 关于生态
- 图的生态怎么打造?和周边其它系统怎么集成融合?

#### 算法和语言

使用 Docker 构建 Nebula Graph 源码

文章分享NebulaGraph 发表了文章 • 0 个评论 • 633 次浏览 • 2019-09-06 10:16 • 来自相关话题

![](https://pic4.zhimg.com/v2-8c5114adb5b955b5a52df78ac2ede317_1200x500.jpg) ### Nebula Graph 介绍 [Nebula ...查看全部
![](https://pic4.zhimg.com/v2-8c5114adb5b955b5a52df78ac2ede317_1200x500.jpg)

### Nebula Graph 介绍

[Nebula Graph](https://0x7.me/go2github) 是开源的高性能分布式图数据库。项目使用 C++ 语言开发,`cmake` 工具构建。其中两个重要的依赖是 Facebook 的 Thrift RPC 框架和 [folly 库](https://github.com/facebook/folly).

由于项目采用了 C++ 14 标准开发,需要使用较新版本的编译器和一些三方库。虽然 Nebula Graph 官方给出了一份[开发者构建指南](https://github.com/vesoft-inc/nebula/blob/master/docs/manual-CN/how-to-build.md),但是在本地构建完整的编译环境依然不是一件轻松的事。

### 开发环境构建

Nebula Graph 依赖较多,且一些第三方库需本地编译安装,为了方便开发者本地编译项目源码, Nebula Graph 官方为大家提供了一个预安装所有依赖的 [docker 镜像]([docker hub](https://hub.docker.com/r/vesoft/nebula-dev))。开发者只需如下的三步即可快速的编译 Nebula Graph 工程,参与 Nebula Graph 的开源贡献:

- 本地安装好 Docker

- 将 [`vesoft/nebula-dev`](https://hub.docker.com/r/vesoft/nebula-dev) 镜像 `pull` 到本地

```shell
$ docker pull vesoft/nebula-dev
```

- 运行 `Docker` 并挂载 Nebula 源码目录到容器的 `/home/nebula` 目录

```shell
$ docker run --rm -ti -v {nebula-root-path}:/home/nebula vesoft/nebula-dev bash
```

> 社区小伙伴@阿东 友情建议:记得把上面的 {nebula-root-path}
替换成你 Nebula Graph 实际 clone 的目录

为了避免每次退出 docker 容器之后,重新键入上述的命令,我们在 [vesoft-inc/nebula-dev-docker](https://github.com/vesoft-inc/nebula-dev-docker.git) 中提供了一个简单的 `build.sh` 脚本,可通过 `./build.sh /path/to/nebula/root/` 进入容器。

- 使用 `cmake` 构建 Nebula 工程

```shell
docker> mkdir _build && cd _build
docker> cmake .. && make -j2
docker> ctest # 执行单元测试
```

### 提醒

Nebula 项目目前主要采用静态依赖的方式编译,加上附加的一些调试信息,所以生产的一些可执行文件会比较占用磁盘空间,建议小伙伴预留 20G 以上的空闲空间给 Nebula 目录 :)

### Docker 加速小 Tips

由于 Docker 镜像文件存储在国外,在 pull 过程中会遇到速度过慢的问题,这里 Nebula Graph 提供一种加速 pull 的方法:通过配置国内地址解决,例如:
- Azure 中国镜像 https://dockerhub.azk8s.cn
- 七牛云 https://reg-mirror.qiniu.com

Linux 小伙伴可在 `/etc/docker/daemon.json` 中加入如下内容(若文件不存在,请新建该文件)

```
{
"registry-mirrors": [
"https://dockerhub.azk8s.cn",
"https://reg-mirror.qiniu.com"
]
}
```
macOS 小伙伴请点击 `Docker Desktop 图标 -> Preferences -> Daemon -> Registry mirrors`。 在列表中添加 `https://dockerhub.azk8s.cn` 和 `https://reg-mirror.qiniu.com` 。修改后,点击 Apply & Restart 按钮, 重启 Docker。

![](https://pic3.zhimg.com/80/v2-6d2dd1b7e5999207ace1b590d31a15ea_hd.jpg)

### Nebula Graph 社区

Nebula Graph 社区是由一群爱好图数据库,共同推进图数据库发展的开发者构成的社区。

本文由 Nebula Graph 社区 Committer 伊兴路贡献,也欢迎阅读本文的你参与到 Nebula Graph 的开发,或向 Nebula Graph 投稿。


### 附录

> Nebula Graph:一个开源的分布式图数据库。

> GitHub:[https://github.com/vesoft-inc/nebula](https://0x7.me/go2github)

> 知乎:https://www.zhihu.com/org/nebulagraph/posts

> 微博:https://weibo.com/nebulagraph

图数据库 Nebula Graph 的安装部署

回复

文章分享NebulaGraph 发起了问题 • 0 人关注 • 0 个回复 • 266 次浏览 • 2019-08-29 20:44 • 来自相关话题

图数据库 Nebula Graph v.1.0.0-beta 已上线

开源程序NebulaGraph 发表了文章 • 0 个评论 • 401 次浏览 • 2019-08-20 20:26 • 来自相关话题

> [Nebula Graph](https://0x7.me/go2github):一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可 ...查看全部
> [Nebula Graph](https://0x7.me/go2github):一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可用且保障数据安全性。

Nebula Graph **v1.0.0-beta** 版本已发布,更新内容如下:

### Storage Engine

- 支持集群部署
- 引入 Raft 一致性协议,支持 Leader 切换
- 存储引擎支持 HBase
- 新增从 HDFS 导入数据功能

### 查询语言 nGQL

- 新增注释功能
- 创建 Space 支持默认属性,新增 `SHOW SPACE` 和 `DROP SPACE` 功能
- 新增获取某 Tag 或 EdgeType 属性功能
- 新增获取某 Tag 或 EdgeType 列表功能
- 新增 `DISTINCT` 过滤重复数据
- 新增 `UNION` , `INTERSECT` 和 `MINUS` 集合运算
- 新增 `FETCH` 获取指定 Vertex 相应 Tag 的属性值
- `WHERE` 和 `YIELD` 支持用户定义变量和管道操作
- `WHERE` 和 `YIELD` 支持算术和逻辑运算
- 新增 `ORDER BY` 对结果集排序
- 支持插入多条点或边
- 支持 HOSTS 的 CRUD 操作

### Tools

- 新增 Java importer - 从 CSV 导入数据
- package_build -  支持 Linux 发行指定版本的软件包
- perf tool - Storage Service 压测工具
- Console 支持关键字自动补全功能

### ChangeLog

- `$$[tag].prop` 变更为 `$$.tag.prop` , `$^[tag].prop` 变更为 `$^.tag.prop` 
- 重构运维脚本 nebula.service

### 附录
最后是 Nebula 的 GitHub 地址,欢迎大家试用,有什么问题可以向我们提 issue。GitHub 地址:[github.com/vesoft-inc/nebula](https://0x7.me/go2github)  ;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot

图数据库综述与 Nebula 在图数据库设计的实践

文章分享NebulaGraph 发表了文章 • 0 个评论 • 708 次浏览 • 2019-08-12 14:16 • 来自相关话题

> [Nebula Graph](https://0x7.me/go2github):一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可 ...查看全部
> [Nebula Graph](https://0x7.me/go2github):一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可用且保障数据安全性。

第三期 nMeetup( nMeetup 全称:Nebula Graph Meetup,为由开源的分布式图数据库 Nebula Graph 发起的面向图数据库爱好者的线下沙龙) 活动于 2019 年 8 月 3 日在上海陆家嘴的汇丰银行大楼举办,我司 CEO -- Sherman 在活动中发表《 Nebula Graph Internals 》主题演讲 。本篇文章是根据此次演讲所整理出的技术干货,全文阅读需要 30 分钟,我们一起打开图数据库的知识大门吧~

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316395313-b80741dd-61c4-4adb-904e-2bd8b53be139.png)

大家好,非常感谢大家今天能够来我们这个线下沙龙,天气很热,刚又下了暴雨,说明大家对图数据库的热情要比夏天温度要高。今天我们准备了几个 topic,一个就是介绍一下我们产品——Nebula 的一些设计思路,今天不讲介绍性东西,主要讲 Nebula 内部的思考——为什么会去做 Nebula,怎么样去做,以及为什么会采取这样的设计思路。

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565337816231-6ee9c2fb-9455-4ff4-b8ab-0495ebfe8686.png)

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316413003-a5405fc3-f736-48b2-99d3-572e6e97e9a3.png#)

这个就是 Nebula。先从 overview 介绍图数据库到底是个什么东西,然后讲我们对图数据库的一些思考。最后具体介绍两个模块, Nebula 的 Query Service 和 Storage Service。后面两部分会稍微偏技术一些,在这个过程当中如果大家遇到什么问题,欢迎随时提出。

## 图数据库是什么
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316422046-0900a5e7-ce5b-4e6c-a227-5d3092243485.png)

### 图领域的 OLAP & OLTP 场景
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316431355-72267be6-228f-4182-ba3f-66f004bf6693.png)

对于图计算或者图数据库本身我们是这么理解的,它跟传统数据库很类似,也分为 OLAP 和 OLTP 两个方向。

上图中下面这根轴表示数据对查询时效性的要求,OLAP 更偏向于做离线分析,OLTP 更偏向于线上处理。我们认为图领域也可以这么划分。

首先在 OLAP 这个领域,我们称为图计算框架,这些图计算框架的主要目的,是做分析。基于图的结构的分析——这里图代指一个关系网络。整体上这个分析比较类似于传统数据库的 OLAP。但一个主要特点就是基于图结构的迭代算法,这个在传统数据库上是没有的。最典型的算法是谷歌 PageRank,通过一些不断的迭代计算,来算网页的相关度,还有非常常见的 LPA 算法,使用的也非常多。

如果我们继续沿着这个线向右边延伸,可以看到一个叫做 Graph Stream 的领域,这个领域相当于图的基础计算跟流式计算相结合的产物。大家知道关系网络并不是单单一个静态结构,而是会在业务中不断地发生变化:可能是图的结构,或者图上的属性发生变化,当图结构和属性发生变化时,我们希望去做一些计算,这里的计算可能是一种触发的计算或判断——例如在变化过程当中是不是动态地在图上形成一个闭环,或者动态地判断图上是否形成一个隔离子图等等。Trigger(触发)的话,一般是通过事件来驱动这类计算。对时效性的响应当然是越高越好,但往往响应时间一般是在秒级左右。

那么再往轴上右边这个方向看,是线上的在线响应的系统。大家知道这类系统的特点是对延时要求非常高。可以想象如果在线上做交易的时候,在这个交易瞬间,需要到图上去拿一些信息,当然不希望要花费秒级。一般响应时间都是在十几、二十毫秒这样一个范围。可以看到最右边的场景和要求跟左边的是完全不一样的,是一种典型的 OLTP 场景。在这种场景里面,通常是对子图的计算或者遍历——这个和左边对全图做计算是完全不一样的:比如说从几个点出发,得到周边3、4度的邻居构成一个子图,再基于这个子图进行计算,根据计算的结果再继续做一些图遍历。所以我们把这种场景称为图数据库。我们现在主要研发内容主要面向OLTP这类场景。所以说今天的一些想法和讲的内容,都是跟这块相关。

### 图数据库及其他数据库的关注度增长趋势

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316446183-1bd35a93-677c-4661-9e9d-9e44cbe89676.png)

这张图是从 DB-Engines.com 上截下来的,反应了从 2013 年到 2019 年 7 月,所有类型数据库的趋势。这个趋势是怎么计算出来的?DB-Engines 通过到各大网站上去爬取内容,查看所有用户,包括开发人员和业务人员的情况下统计某类数据库被提及的次数将其转化为分数,分数越高,表示这种类型的数据库的关注度越高。所以说这是一个关注度的趋势。

最底下这条红线是关系型数据库,在关系型数据库之上有各类数据库,比如 Key-Value型、文档型、RDF 等。最上面的绿线就是图数据库。

可以看到在过去六年多的时间里,图数据库的整个趋势,或者说它的影响力大概增长了十倍。

### 图数据存储计算什么数据
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316455530-b367bb83-93c2-4922-ba51-97dbdd5ce7d9.png)

今天我们谈数据库肯定离不开数据,因为数据库只是一个载体,一个存储和计算的载体,它里面的数据到底是什么呢?就是我们平时说的图。这里列出了几个目前为止比较常见的多对多的关系数据。

#### 图数据的常见多对多关系数据库场景

第一个 Social Network(社交网路),比如说微信或者 Facebook 好友关系等等。这个网络有几十亿个用户,几千亿到几万亿的连接关系。第二个 Business Relation,商业的关系,常见的有两种网络:

- 金融与资金关系网络:一种比如说在支付网络里面,账户和账户之间的支付关系或者转账关系,这个是比较典型的金融与资金关系网络;
- 公司关系:在 business 里面,比如说公司控股关系,法人关系等等,它也是一个非常庞大的网络。基于工商总局数据,有许多公司耕耘在这一领域。这个网络的节点规模也有亿到十亿的级别,大概几百亿条边,如果算上交易转账数据那就非常庞大了。

第三个是知识图谱,也是最近比较热的一个领域。在各个垂直领域会有不同的知识点,且知识点之间有相关性。部分垂直领域知识的网络至少有几百亿条关系,比如银行、公安还有医学领域。

最后就是这几年热门的 IoT(Internet of Things)领域,随着近年智能设备的增长,预计以后 IoT 设备数量会远超过人口数量,现在我们每个人身边佩带的智能设备已不止一个,比如说智能手机、智能手表,它们之间组成一个庞大的关系网络,虽然具体应用有待后续开发,但这个领域在未来会有很大的应用空间。

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316464166-e5aeeea1-77df-46ce-9a65-893f1d1c8974.png)

#### 图数据库的应用场景

刚才提到的是常见的关系网络,这里是我们思考的一些应用场景。

第一个应用场景是基于社交关系网络的社交推荐,比如:拼多多的商品推荐,抖音的视频推荐,头条的内容推荐,都可以基于已有的好友关系来推荐。

第二个就是风控领域,风控其实是银行保险业的核心话题。传统的风控是基于规则——基于规则的风控手段,相对已经比较成熟了,一般是拿直接的交易对手来做规则判断。但现在风控有个新趋势,就是通过关联关系做拓展,比如交易对手等相关的周边账号,通过这些关系来判断这笔交易或者转账的风险。从规则向基于关联关系的风控演进,这个趋势比较明显。

关于知识图谱这一块,和 Google 比较有关,谷歌在 2003,2004 年时候,其实已经在慢慢把它的 search engine,从反向索引转向转到了知识图谱。如果只有倒排表,比如说要查“特朗普”今年几岁,这个是很难做到的,因为已有的信息是他的生日是哪年。

这几年机器学习和 AI 领域发展非常快,大家知道就机器学习或者模型训练范畴来说,平时用大量数据去训练模型,其实归根结底是对大量数据汇总或者说统计性的结果。最近一两年,大家发现光有统计性结果不够,数据和数据之间的关系也应体现在模型里,所以开始将基于图的数据关系加入到模型训练,这个就是学术界非常流行的 Graph Embedding,把图的结构引入到模型训练里面。

在健康和医疗领域,患者的过往病史、服药史、医生的处方还存在纸质文档的情况,一些医疗类公司通过语音和图像将文档数字化,再用 NLP 把关键信息提取出来。根据关键信息,比如:血压、用药等等构造一棵大的决策树或者医疗知识图谱。这块也是比较新的应用。

区块链的应用其实比较容易理解,区块链本身虽然说是链,但有很多分支结构,当分支交织后也就构成一个网络。举个简单例子,A 某想通过比特币洗钱,常用方法是通过多个账号,几次转账后,资金通过数字货币形成一个闭环,而这个方法是可以通过图进行洗钱防范。

最后一块是公共安全领域,比如,某些犯罪是团伙作案,那么追踪团伙中某个人的行为轨迹,比如:交通工具、酒店等等就可标识出整个团伙的特征。某个摄像头和某个嫌疑人在某个时间构建起来关联关系,下一个时刻,另外一个摄像头和另外一个嫌疑人也建立了关联。这个图不是静态的,它是时序的。
这些就是一些已经看到的图的应用领域。

### 图数据库面临的挑战

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316479943-3417553e-2e80-4551-95b6-c598c892eca9.png)

回到图数据库,做图数据库到底有哪些挑战。和所有的 OLTP 系统一样:

第一个挑战就是**低延时**。我们不希望一次查询,要几秒钟甚至几分钟才能产生结果。比如说风控场景,在线转账的时候,我要判断这笔交易是否有风险,可能整个时间只有一百毫秒,留给风控判断的时间只有几十毫秒。不可能转账完才发现对方账户已经被标黑了,或者这笔交易其实是在套null现。

第二个挑战是**高吞吐**,现在热门的 APP,比如抖音或者头条,日常访问的并发量是非常高的,峰值可能几十万 QPS,DB 要能抗的住。

第三个挑战是**数据量激增**,数据量的增加速度快于硬件特别是硬盘的增长速度,这个给 DB 带来了很大的挑战。大家可能用过一些单机版的图数据库,刚开始用觉得不错,能满足需求。但一两年后,发现数据量增加太快,单机版已经完全满足不了需求,这时总不能把业务流控吧。

我们遇到过一个图 case,有超过一千亿个节点,一万亿条边,点和边上都还有属性,整个图的数据量超过上百T。可以预计下,未来几年数据量的增长速度会远远快于摩尔定理的速度,所以单机版数据库肯定搞不定业务需求,这对图数据库开发是一个很大的挑战。

第四个挑战是**分析的复杂性**,当然这里分析指的是 OLTP 层面的。因为图数据库还比较新,大家刚开始使用的时候会比较简单,例如只做一些 K度拓展。但是随着使用者的理解越来越深,就会提出更多越来越复杂的需求。例如在图遍历过程中过滤、统计、排序、循环等等,再根据这些计算结果继续图遍历。所以说业务需求越来越复杂。这就要求图数据库提供的功能越来越多。

最后一个挑战是关于**数据一致性**——当然还有很多其他挑战,这里没有全部罗列。前几年大家对于图数据库的使用方法更像使用二级索引,把较大的数据放在另外的存储组件,比如 HBase 将关联关系放在图数据库里,将图数据库只作为图结构索引来加速。但像刚才说的,业务越来越复杂,对响应时间要求越来越高,原先的架构除了不方便,性能上也有很大挑战。比如,需要对属性做过滤时,要从 HBase 读取出太多数据,各种序列化、过滤操作都很慢。这样就产生了新需求——将这些数据直接存储在图数据库里,自然 ACID 的需求也都有了。

### 图数据库模型:原生图数据库 vs 多模数据库
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316491849-b28c48e1-d71b-4c99-850b-0f3c80481825.png)

说完技术挑战,还有个概念我想特别澄清下。大家如果网上搜图数据库,可能有 20 个自称图数据库的产品。我认为这些产品可以分成两类,一种就是**原生**的,还有一类是**多模**的。

对于图原生的产品,在设计时考虑了图数据的特性,存储、计算引擎都是基于图的特点做了特别设计和优化。

而对于多模的产品,就有很多,比如说 ArangoDB 或者 Orientdb,还有一些云厂商的服务。它们的底层是一个表或文档型数据库,在上层增加图的服务。对于这类多模数据库,图服务层所做的操作,比如:遍历、写入,最终将被映射到下面的存储层,成为一系列表和文档的操作。所以它最大的问题是整个系统并不是为了图这种多对多的结构特点设计,一旦数据量或者并发量增大之后,问题就比较明显。我们最近碰到一个比较典型的 case,客户使用多模 DB,在数据量很小时还比较方便,但当数据量大到一定程度,做二跳三跳查询时 touch 的数据非常多,而多模 DB 底层是关系型数据库,所有关系最终要映射到关系型数据库的 `join`  操作,做三四层的 `join` ,性能会非常的差。

### 图数据库——Nebula Graph:一个开源的分布式图数据库
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316501138-7ff22a15-8912-4877-9e9d-fb8102e8039d.png)

上面是我们对行业的一些思考。这里是我们在做的图数据库,它是一个开源的分布式的项目——[Nebula Graph](https://github.com/vesoft-inc/nebula)。

#### 存储设计
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316509130-47d07bbb-1d33-4a1a-90f6-ffa600d1b0b2.png)

这里我想说下我们在设计 Nebula 时候的一些思考,为什么会这样设计?

刚刚说到过技术挑战中数据量迅速膨胀,业务逻辑越发复杂,像这样的开发挑战,Nebula 是如何解决的。

Nebula 在设计存储时,采用 share-nothing 的分布式架构,本质上存储节点间没有数据共享,也就是整个分布式结构无中心节点。这样的好处在于,第一,容易做水平拓展;第二,即使部分机器 Crash,通过数据强一致性—— Raft 协议能保证整个系统的可用性,不会丢失数据。

因为业务会越发复杂,所以 Nebula 支持多种存储引擎。有的业务数据量不大但对查询的实时性要求高,Nebula 支持纯内存式的存储引擎;数据量大且查询并发量也大的情况下,Nebula 支持使用内存加 SSD 模式。当然 Nebula 也支持第三方存储引擎,比如,HBase,考虑到这样使用存在的主要问题是性能不佳,我们建议用在一些对性能要求不是很高的场景。

第三个设计就是把存储和计算这两层分开了——也就是“存储计算分离”。这样的设计有几个明显的好处。所有数据库在计算层通常都是无状态的,CPU intensive,当 CPU 的计算力不够的时候,容易弹性扩容、缩容,而对于存储层而言,涉及到数据的搬迁情况要复杂些。所以当存储计算分离后,计算层和存储层可以根据各自的情况弹性调整。

至于数据强一致这个挑战,有主要分两个方面,一个是关于数据的强一致,就是多数派协议——Nebula 现在使用的 Raft 协议,通过多副本的方式来实现强一致。另外个是分布式的事物务 Transaction,它来保证要向多台机器写入一批相互依赖数据的正确性,这个和 NewSQL 里面的概念是非常类似的。

#### 计算设计
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316533389-f15a9475-b9ac-4a59-bbc3-55c52d2366f1.png)

刚刚是我们对存储引擎的一些思考,这里是我们对计算引擎的思考。

前面提到的一个技术挑战是低延时、高并发,Nebula 整个的核心代码都是 C++ 写的,这样保证了执行效率。其次,做了很多并行和异步执行的优化。第三个是计算下推。在分布式系统里面,硬件上网络对整体性能的影响最大,所以数据搬迁是一个很低效的动作。有些开源图数据库产品,比如 JanusGraph,它的存储层在 HBase,上面有个单独的计算层,当计算层需要数据的时候,会到 HBase 里面拉回大量的数据,再做过滤和计算。举个例子,1 万条数据里面最终过滤出 100 条,那相当于 99% 的网络传输都浪费了。所以 Nebula 的设计方案是移动计算,而不是数据,计算下推到存储层,像前面这个例子,直接在存储层做完过滤再回传计算层,这样可以有 100 倍的加速。

第二,如果大家接触过图数据库领域的一些产品,会发现图数据库这领域,相比关系型数据库有个很大的问题——没有通用的标准。关系型领域的标准在差不多 30 年前已制定,但图数据库这个领域各家产品的语言相差很大。那么针对这个问题 Nebula 是怎么解决?第一尽量贴近 SQL,哪怕你没有学过 Nebula 语言,你也能猜出语句的作用。因此 Nebula 的查询语言和 SQL 很像,为描述性语言,而不是命令式语言。第二个是过去几年我们做图数据库领域的经验积累,就是 No-embedding(无嵌套)。SQL 是允许 embedding 的,但嵌套有个问题——当查询语句过长时,语句难读,因为 SQL 语句需从内向外读,语序正好跟人的理解相反,因为人比较习惯从前往后来理解。所以Nebula 把嵌套语句写成从前往后的方式,作为替代,提供 Shell 管道这样的方式,即前面一条命令的输出作为后一条命令的输入。这样写出来的语句比较容易理解,写了一个上百行的 query 你就会发现从前往后读比从中间开始读要易于理解。
第三,和传统数据库相比,图的计算不光是 CRUD,更重要是基于图结构的算法,加上新的图算法不停地涌现,Nebula 怎么 keep up?

- 将部分主要算法 build in 到查询引擎里;
- 通过支持 UDF(user-defined function,用户定义函数),用户可把业务相关逻辑写成程序或者函数,避免写重复 query;
- 查询语言的编程能力:Nebula 的查询语言 nGQL 是 Programmable。

#### 架构设计
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316561369-18ed8110-1928-42d5-aea3-0b41aa1ee4ff.png)

Overview 这一章节的最后内容是 Nebula 的架构。上图虚线把存储和计算一分为二,上面是整个计算层,每一台机器或者虚拟机是一个节点,它是无状态的,相互之间没有任何通讯,所以计算层要做扩缩容非常容易。
下面是存储层,有数据存储所以是有状态的。存储层还可以细分,第一层是 Storage Service,从计算层传来的请求带有图语义信息,比如:邻居点、取 property,Storage Service 的作用是把图语义变成了 key-value 语义交给下层的 KV-store,在 Storage Service 和 KV-store 之间是一层分布式 Raft 协议。

图的右边是 Meta Service,这个模块有点类似 HDFS 的 NameNode,主要提供两个功能,一个是各种元信息,比如 schema,还有一个是指挥存储扩容。大家都知道存储扩容是个非常复杂的过程,在不影响在线业务地情况下一步步增加机器,数据也要一点点搬迁,这个时候就需要有个中心指挥。另外 Meta Service 本身也是通过 Raft 来保证高可用的。

## 图数据库 Nebula 的查询引擎设计
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316581459-ce28c72c-acc0-4f36-9b2c-91613a2ef28d.png)

上面就是 Nebula 的总体介绍,下面这个部分介绍查询引擎的设计细节。

### 查询语言——nGQL
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316588889-b39614ec-2d99-48a4-b334-1533a5e1692b.png)

先来介绍下 Nebula 的查询语言 nGQL。nGQL 的子查询有三种组合方式:管道、分号和变量。
nGQL 支持实时的增删改、图遍历、属性遍历,也支持对属性先做 index 再遍历。此外,你还可以对图上的路径写个正则表达式,查找所有满足这个条件的图路径。

### 查询引擎架构
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316603924-6d989dd0-2343-4a88-b1c6-834da892400b.png)

再来介绍下查询引擎的架构,从查询引擎的架构图上来看,和数据库类似:从客户端发起一个请求(statement),然后 Parser 做词法分析,之后把分析结果传给执行计划(Execution Planner),执行计划会基于这个语句的特点,交给优化器(Optimizer)进行优化,最后将结果交给执行引擎(Execution Engine)去执行。执行引擎会通过 MetaService 获取点和边的 schema,通过存储引擎层获取点和边的数据。

### 执行计划案例
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316612138-e1cca82e-3d07-47d9-906d-e3d7ac17cc08.png)

这里有个小例子展示了执行计划,下方就是一条语句。首先 use myspace,这里的 space 和数据库里的 database 是一个概念,每个 space 是一个物理隔离的空间,可用来区分敏感数据和非敏感数据,或者说做多租户的支持。分号后面是下一语句—— `INSERT VERTEX` 插入节点, `GO` 是网络拓展, `|` 为 Nebula 的管道用法,这条语句的意思是将第一条 Go 的结果传给第二条 Go,然后再传给第三条,即往外走三步遍历,最后把整个结果做求和运算。这是一种常见的写法,Nebula 还支持其他写法。

这样语句的执行计划就会变成上图右边的语法执行树。这里每个节点,都叫做Executor(语法执行者),语句中的分号对应执行 `SequentialExecutor` ,Go 对应执行 `GoExecutor` ,"|"(管道)对应执行 `PipeExecutor` 。

### 执行优化
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316621670-da45b911-47d0-4749-a37f-b5aefbddffb2.png)

这一页介绍执行优化。在顺序执行过程中优化器会判断当前语句是否存在相互依赖关系,在没有相互依赖时,执行引擎可并行执行从而加速整个执行过程,降低执行延时。流水线优化,跟处理器 CPU 的流水线优化类似。上面“GO … | GO … | GO … ”例子中,表面上第一个 GO 执行完毕后再把结果发给第二个 GO 执行,但实际执行时,第一个 GO 部分结果出来之后,就可以先传给下一个GO,不需要等全部结果拿到之后再传给下一步,这对提升时延效果明显。当然不是所有情况都能这样优化,里面有很多工作要做。前面已提过计算下沉的优化,即过滤的操作尽量放在存储层,节省通过网络传输的数据量。JIT 优化在数据库里已经比较流行,把一条 query 变成代码直接去执行,这样执行效率会更高。

## 图数据库 Nebula 的存储引擎设计
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316631002-29a58273-ce4e-4acd-bec7-0eab273d2c6f.png)

刚才介绍了查询引擎,下面介绍存储引擎。

### 存储架构
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316637994-b7fc8669-f01d-4806-9be8-82d9bdf21296.png)

这张图其实是前面整体架构图的下面部分。纵向可理解为一台机器,每台机器最上面是 Storage service,绿色的桶是数据存储,数据被切分成很多个分片 partition,3 台机器上同 id 的 partition 组成一个 group,每个 group 内用 Raft 协议实现多副本一致性。Partition 的数据最后落在 Store Engine 上,Nebula 默认 Store Engine 是 RocksDB。这里再提一下,partition 是个逻辑概念,partition 数量多少不会额外增加内存,所以一般把 partition 数量设置成远大于机器的数量。

### Schema
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316646278-5ded0550-c211-4fe5-b86e-9a2dc40b6ef3.png)

这一页是 schema,讲的是怎么把图数据变成 KV 存储。这里面的第一个概念是标签(Tag),“标签”表示的是点(Vertex)的类型,一个点可以有多种“标签”或者说“类型”。另一个概念是边类型(Edge Type),一条边需要用起点 ID,终点 ID,边类型和 Ranking 来唯一标识。前面几个字段比较好理解,Ranking 这个字段是留给客户表示额外信息,比如:转账时间。这里补充下说明下 Nebula 顶点 Vertex、标签 Tag、边 Edge、边类型 Edge Type的关系。

Vertex 是一个顶点,用一个 64 位的 id 来标识,一个 Vertex 可以被打上多个 Tag(标签),每个 Tag 定义了一组属性,举个例子,我们可以有 Person 和 Developer 这两个 Tag,Person 这个 Tag 里定义了姓名、电话、住址等等信息,Developer 这个 Tag 里可能定义了熟悉的编程语言、工作年限、GitHub 账号等等信息。一个 Vertex 可以被打上 Person 这个 Tag,这就表示这个 Vertex 代表了一个 Person,同时也包含了 Person 里的属性。另一个 Vertex 可能被同时打上了 Person 和 Developer 这两个 Tag,那就表示这个 Vertex 不仅是一个 Person,还是一个 Developer。

Vertex 和 Vertex 之间可以用 Edge 相连,每一条 Edge 都会有类型,比如:好友关系。每个 Edge Type 可定义一组属性。Edge 一般用来表示一种关系,或者一个动作。比如,Peraon A 给 Person B 转了一笔钱,那 A 和 B 之间就会有一条 Transfer 类型的边,Transfer 这个边类型(Edge Type)可以定义一组属性,比如转账金额,转账时间等等。任何两个 Vertex 之间可以有多种类型的边,也可以有多条同种类型的边,比如:转账,两个 Person 间可存在多笔转账,那每笔转账就是一条边。
上面例子中,点和边都带有属性,即多组。Nebula是一个强 schema 系统,属性的每个字段名和对应的类型需要在构图时先定义,和数据库中的 alter table 类似 Nebula 也支持你在后续操作中进行 Schema 更改。Schema 中还有常见的 TTL(Time To Live),指定特定数据的生命周期,数据时效到后这条数据会被自动清理。

### 数据分片和 Key 的设计
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316665777-8c9e2593-9758-4d9d-a744-7e6617aa5d74.png)

刚才有提到过分片(Partition)和键(Key)的设计,这里再详细解释一下。

数据分片 Partition 或者有些系统叫 Shard,它的 ID 是怎么得到?非常简单,根据点的 ID 做 Hash,然后取模 partition 数量,就得到了 PartitionID。

Vertex 的 Key 是由 PartID + VID + TagID 三元组构成的,Value 里面存放的是属性(Property),这些属性是一系列 KV 对的序列化。

Edge 的 Key 是由 PartID + SrcID + EdgeType + Ranking + DstID 五元组构成,但边和点不同:一个点在 Nebula 里只存储一个 KV,但在 Nebula 中一条边会存两个 KV,一个 Out-edge Key和一个 In-edge Key,Out-edge 为图论中的出边,In-edge 为图论中的入边,虽然两个 Key 都表示同一条逻辑边,但存储两个 KV 的好处在于遍历时,方便做出度和入度的遍历。

举个例子:要查询过去 24 小时给我转过钱的人,即查找所有指向我的账号,遍历的时候从“我”这个节点开始,沿着边反向走可以看到 Key 的设计是入边 In-edge 的 DstID 信息在前面。所以做 Key 查询时,入边和终点,也就是“我”和“指向我的边”是存储在一个分片 Partition 上的,这样就不涉及跨网络开销,最多一次硬盘读就可以取到对应数据。

### 多副本和高可用
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316704699-fb374731-de27-4666-9a30-67d46d1d002b.png)

最后再谈下数据多副本和 Failover 的问题。前面已经提到多副本间是基于 Raft 协议的数据强一致。Nebula 在 Raft 做了些改进,比如,每组 partition 的 leader 都是打散的,这样才可以在多台机器并发操作。此外还增加了 Raft learner 的角色,这个角色不会参与 Raft 协议的投票,只负责从 leader 读数据。这个主要应用于一个数据中心向另外一个数据中心做异步复制场景,也可用于复制到另外第三方存储引擎,比如:HBase。

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316716467-bb028c7d-b121-4520-b561-996499609708.png)

### 容错机制
Nebula 对于容错或者说高可用的保证,主要依赖于 Raft 协议。这样单机 Crash 对服务是没有影响的,因为用了 3 副本。那要是 Meta Server 挂了,也不会像 HDFS 的 NameNode 挂了影响那么大,这时只是不能新建 schema,但是数据读写没有影响,这样做 meta 的迁移或者扩容也比较方便。

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316723938-af9627d5-caba-4b43-885b-8fb9b73936e1.png)

最后是 Nebula 的 GitHub 地址,欢迎大家试用,有什么问题可以向我们提 issue。GitHub 地址:[github.com/vesoft-inc/nebula](https://0x7.me/go2github)

> Nebula Graph:一个开源的分布式图数据库。

> GitHub:[github.com/vesoft-inc/nebula](https://0x7.me/go2github)

> 知乎:https://www.zhihu.com/org/nebulagraph/posts

> 微博:https://weibo.com/nebulagraph

go + koa = ? 一个新的web框架 goa 诞生

Go开源项目nicholascao 发表了文章 • 1 个评论 • 491 次浏览 • 2019-08-08 15:52 • 来自相关话题

## koajs 相信绝大部分使用nodejs的开发者都知道[koa](https://koa.bootcss.com/),甚至每天都在跟koa打交道。 ## goa 最近因工作需要 ...查看全部
## koajs

相信绝大部分使用nodejs的开发者都知道[koa](https://koa.bootcss.com/),甚至每天都在跟koa打交道。

## goa

最近因工作需要从nodejs转到go,因此开发了一个koa for golang的web框架--goa。
几乎一样的语法,一样基于中间件。

github地址:[goa](https://github.com/goa-go/goa)

demo:

``` golang
package main

import (
"fmt"
"time"

"github.com/goa-go/goa"
"github.com/goa-go/goa/router"
)

func logger(c *goa.Context, next func()) {
start := time.Now()

fmt.Printf("[%s] <-- %s %s\n", start.Format("2006-6-2 15:04:05"), c.Method, c.URL)
next()
fmt.Printf("[%s] --> %s %s %d%s\n", time.Now().Format("2006-6-2 15:04:05"), c.Method, c.URL, time.Since(start).Nanoseconds()/1e6, "ms")
}

func json(c *goa.Context) {
c.JSON(goa.M{
"string": "string",
"int": 1,
"json": goa.M{
"key": "value",
},
})
}

func main() {
app := goa.New()
router := router.New()

router.GET("/", func(c *goa.Context) {
c.String("hello world")
})
router.GET("/json", json)

app.Use(logger)
app.Use(router.Routes())
app.Listen(":3000")
}
```
如果觉得这个项目不错的话,请给个star给予作者鼓励,
另外欢迎fork和加入开发团队共建。

再次贴上地址https://github.com/goa-go/goa

【Zinx第五章-消息封装】Golang轻量级并发服务器框架

回复

文章分享Aceld 发起了问题 • 1 人关注 • 0 个回复 • 330 次浏览 • 2019-04-29 11:53 • 来自相关话题

【Zinx第四章-全局配置】Golang轻量级并发服务器框架

回复

文章分享Aceld 发起了问题 • 1 人关注 • 0 个回复 • 274 次浏览 • 2019-04-29 11:52 • 来自相关话题

【Zinx第三章-基础路由模块】Golang轻量级并发服务器框架

回复

文章分享Aceld 发起了问题 • 1 人关注 • 0 个回复 • 253 次浏览 • 2019-04-29 11:50 • 来自相关话题

条新动态, 点击查看
zdreamx

zdreamx 回答了问题 • 2016-11-08 11:27 • 26 个回复 不感兴趣

beego1.8版本功能征集

赞同来自:

orm有没有考虑分表分区分库的优化或支持
orm有没有考虑分表分区分库的优化或支持

图数据库 Nebula Graph 的安装部署

回复

文章分享NebulaGraph 发起了问题 • 0 人关注 • 0 个回复 • 266 次浏览 • 2019-08-29 20:44 • 来自相关话题

【Zinx第五章-消息封装】Golang轻量级并发服务器框架

回复

文章分享Aceld 发起了问题 • 1 人关注 • 0 个回复 • 330 次浏览 • 2019-04-29 11:53 • 来自相关话题

【Zinx第四章-全局配置】Golang轻量级并发服务器框架

回复

文章分享Aceld 发起了问题 • 1 人关注 • 0 个回复 • 274 次浏览 • 2019-04-29 11:52 • 来自相关话题

【Zinx第三章-基础路由模块】Golang轻量级并发服务器框架

回复

文章分享Aceld 发起了问题 • 1 人关注 • 0 个回复 • 253 次浏览 • 2019-04-29 11:50 • 来自相关话题

【Zinx第二章-初识Zinx框架】Golang轻量级并发服务器框架

回复

文章分享Aceld 发起了问题 • 1 人关注 • 0 个回复 • 314 次浏览 • 2019-04-29 11:48 • 来自相关话题

Mastering Go翻译项目召集

回复

Go开源项目Cloud001 发起了问题 • 4 人关注 • 0 个回复 • 625 次浏览 • 2019-01-04 21:58 • 来自相关话题

beego 开发轻博客 实战教程

回复

文章分享够浪 回复了问题 • 7 人关注 • 1 个回复 • 2475 次浏览 • 2018-09-29 20:15 • 来自相关话题

拯救你的代码

回复

开源程序lichao2018 回复了问题 • 3 人关注 • 2 个回复 • 3512 次浏览 • 2018-02-07 09:43 • 来自相关话题

请问有没有比较好的分布式系统监控项目?

回复

开源程序fiisio 回复了问题 • 10 人关注 • 4 个回复 • 4547 次浏览 • 2017-08-21 10:41 • 来自相关话题

beego1.8版本功能征集

回复

文章分享lkhjlbh 回复了问题 • 23 人关注 • 26 个回复 • 9370 次浏览 • 2017-03-22 17:58 • 来自相关话题

请问有没有比较实用的go日志分析程序?

回复

有问必答九命猫 回复了问题 • 7 人关注 • 4 个回复 • 6948 次浏览 • 2016-12-06 13:52 • 来自相关话题

有没有好用的开源CDN服务?

回复

有问必答astaxie 回复了问题 • 4 人关注 • 1 个回复 • 5064 次浏览 • 2016-10-18 16:40 • 来自相关话题

Go netpoll I/O 多路复用构建原生网络模型之源码深度解析

文章分享panjf2000 发表了文章 • 0 个评论 • 311 次浏览 • 4 天前 • 来自相关话题

原文Go netpoll I/O 多路复用构建原生网络模型之源码深度解析 ...查看全部

原文

Go netpoll I/O 多路复用构建原生网络模型之源码深度解析

导言

Go 基于 I/O multiplexing 和 goroutine 构建了一个简洁而高性能的原生网络模型(基于 Go 的I/O 多路复用 netpoll),提供了 goroutine-per-connection 这样简单的网络编程模式。在这种模式下,开发者使用的是同步的模式去编写异步的逻辑,极大地降低了开发者编写网络应用时的心智负担,且借助于 Go runtime scheduler 对 goroutines 的高效调度,这个原生网络模型不论从适用性还是性能上都足以满足绝大部分的应用场景。

然而,在工程性上能做到如此高的普适性和兼容性,最终暴露给开发者提供接口/模式如此简洁,其底层必然是基于非常复杂的封装,做了很多取舍,也有可能放弃了一些『极致』的设计和理念。事实上netpoll底层就是基于 epoll/kqueue/iocp 这些系统调用来做封装的,最终暴露出 goroutine-per-connection 这样的极简的开发模式给使用者。

Go netpoll 在不同的操作系统,其底层使用的 I/O 多路复用技术也不一样,可以从 Go 源码目录结构和对应代码文件了解 Go 在不同平台下的网络 I/O 模式的实现。比如,在 Linux 系统下基于 epoll,freeBSD 系统下基于 kqueue,以及 Windows 系统下基于 iocp。

本文将基于 linux 平台来解析 Go netpoll 之 I/O 多路复用的底层是如何基于 epoll 封装实现的,从源码层层推进,全面而深度地解析 Go netpoll 的设计理念和实现原理,以及 Go 是如何利用netpoll来构建它的原生网络模型的。主要涉及到的一些概念:I/O 模式、用户/内核空间、epoll、linux 源码、goroutine scheduler 等等,我会尽量简单地讲解,如果有对相关概念不熟悉的同学,还是希望能提前熟悉一下。

用户空间与内核空间

现在操作系统都是采用虚拟存储器,那么对 32 位操作系统而言,它的寻址空间(虚拟存储空间)为 4G(2 的 32 次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对 linux 操作系统而言,将最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF),供内核使用,称为内核空间,而将较低的 3G 字节(从虚拟地址 0x00000000 到0xBFFFFFFF),供各个进程使用,称为用户空间。

I/O 多路复用

在神作《UNIX 网络编程》里,总结归纳了 5 种 I/O 模型,包括同步和异步 I/O:

  • 阻塞 I/O (Blocking I/O)
  • 非阻塞 I/O (Nonblocking I/O)
  • I/O 多路复用 (I/O multiplexing)
  • 信号驱动 I/O (Signal driven I/O)
  • 异步 I/O (Asynchronous I/O)

操作系统上的 I/O 是用户空间和内核空间的数据交互,因此 I/O 操作通常包含以下两个步骤:

  1. 等待网络数据到达网卡(读就绪)/等待网卡可写(写就绪) –> 读取/写入到内核缓冲区
  2. 从内核缓冲区复制数据 –> 用户空间(读)/从用户空间复制数据 -> 内核缓冲区(写)

而判定一个 I/O 模型是同步还是异步,主要看第二步:数据在用户和内核空间之间复制的时候是不是会阻塞当前进程,如果会,则是同步 I/O,否则,就是异步 I/O。基于这个原则,这 5 种 I/O 模型中只有一种异步 I/O 模型:Asynchronous I/O,其余都是同步 I/O 模型。

这 5 种 I/O 模型的对比如下:

所谓 I/O 多路复用指的就是 select/poll/epoll 这一系列的多路选择器:支持单一线程同时监听多个文件描述符(I/O事件),阻塞等待,并在其中某个文件描述符可读写时收到通知。 I/O 复用其实复用的不是 I/O 连接,而是复用线程,让一个 thread of control 能够处理多个连接(I/O 事件)。

select & poll

#include <sys/select.h>

/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

// 和 select 紧密结合的四个宏:
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

select 是 epoll 之前 linux 使用的 I/O 事件驱动技术。

理解 select 的关键在于理解 fd_set,为说明方便,取 fd_set 长度为 1 字节,fd_set 中的每一 bit 可以对应一个文件描述符 fd,则 1 字节长的 fd_set 最大可以对应 8 个 fd。select 的调用过程如下:

  1. 执行 FD_ZERO(&set), 则 set 用位表示是 0000,0000
  2. 若 fd=5, 执行 FD_SET(fd, &set); 后 set 变为 0001,0000(第5位置为1)
  3. 再加入 fd=2, fd=1,则 set 变为 0001,0011
  4. 执行 select(6, &set, 0, 0, 0) 阻塞等待
  5. 若 fd=1, fd=2 上都发生可读事件,则 select 返回,此时 set 变为 0000,0011 (注意:没有事件发生的 fd=5 被清空)

基于上面的调用过程,可以得出 select 的特点:

  • 可监控的文件描述符个数取决于 sizeof(fd_set) 的值。假设服务器上 sizeof(fd_set)=512,每 bit 表示一个文件描述符,则服务器上支持的最大文件描述符是 512*8=4096。fd_set的大小调整可参考 【原创】技术系列之 网络模型(二) 中的模型 2,可以有效突破 select 可监控的文件描述符上限
  • 将 fd 加入 select 监控集的同时,还要再使用一个数据结构 array 保存放到 select 监控集中的 fd,一是用于在 select 返回后,array 作为源数据和 fd_set 进行 FD_ISSET 判断。二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,则每次开始 select 前都要重新从 array 取得 fd 逐一加入(FD_ZERO最先),扫描 array 的同时取得 fd 最大值 maxfd,用于 select 的第一个参数
  • 可见 select 模型必须在 select 前循环 array(加 fd,取 maxfd),select 返回后循环 array(FD_ISSET判断是否有事件发生)

所以,select 有如下的缺点:

  1. 最大并发数限制:使用 32 个整数的 32 位,即 32*32=1024 来标识 fd,虽然可修改,但是有以下第 2, 3 点的瓶颈
  2. 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
  3. 性能衰减严重:每次 kernel 都需要线性扫描整个 fd_set,所以随着监控的描述符 fd 数量增长,其 I/O 性能会线性下降

poll 的实现和 select 非常相似,只是描述 fd 集合的方式不同,poll 使用 pollfd 结构而不是 select 的 fd_set 结构,poll 解决了最大文件描述符数量限制的问题,但是同样需要从用户态拷贝所有的 fd 到内核态,也需要线性遍历所有的 fd 集合,所以它和 select 只是实现细节上的区分,并没有本质上的区别。

epoll

epoll 是 linux kernel 2.6 之后引入的新 I/O 事件驱动技术,I/O 多路复用的核心设计是 1 个线程处理所有连接的等待消息准备好I/O 事件,这一点上 epoll 和 select&poll 是大同小异的。但 select&poll 预估错误了一件事,当数十万并发连接存在时,可能每一毫秒只有数百个活跃的连接,同时其余数十万连接在这一毫秒是非活跃的。select&poll 的使用方法是这样的:返回的活跃连接 == select(全部待监控的连接)

什么时候会调用 select&poll 呢?在你认为需要找出有报文到达的活跃连接时,就应该调用。所以,select&poll 在高并发时是会被频繁调用的。这样,这个频繁调用的方法就很有必要看看它是否有效率,因为,它的轻微效率损失都会被高频二字所放大。它有效率损失吗?显而易见,全部待监控连接是数以十万计的,返回的只是数百个活跃连接,这本身就是无效率的表现。被放大后就会发现,处理并发上万个连接时,select&poll 就完全力不从心了。这个时候就该 epoll 上场了,epoll 通过一些新的设计和优化,基本上解决了 select&poll 的问题。

epoll 的 API 非常简洁,涉及到的只有 3 个系统调用:

#include <sys/epoll.h>  
int epoll_create(int size); // int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

其中,epoll_create 创建一个 epoll 实例并返回 epollfd;epoll_ctl 注册 file descriptor 等待的 I/O 事件(比如 EPOLLIN、EPOLLOUT 等) 到 epoll 实例上;epoll_wait 则是阻塞监听 epoll 实例上所有的 file descriptor 的 I/O 事件,它接收一个用户空间上的一块内存地址 (events 数组),kernel 会在有 I/O 事件发生的时候把文件描述符列表复制到这块内存地址上,然后 epoll_wait 解除阻塞并返回,最后用户空间上的程序就可以对相应的 fd 进行读写了:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

epoll 的工作原理如下:

与 select&poll 相比,epoll 分清了高频调用和低频调用。例如,epoll_ctl 相对来说就是不太频繁被调用的,而 epoll_wait 则是非常频繁被调用的。所以 epoll 利用 epoll_ctl 来插入或者删除一个 fd,实现用户态到内核态的数据拷贝,这确保了每一个 fd 在其生命周期只需要被拷贝一次,而不是每次调用 epoll_wait 的时候都拷贝一次。 epoll_wait 则被设计成几乎没有入参的调用,相比 select&poll 需要把全部监听的 fd 集合从用户态拷贝至内核态的做法,epoll 的效率就高出了一大截。

在实现上 epoll 采用红黑树来存储所有监听的 fd,而红黑树本身插入和删除性能比较稳定,时间复杂度 O(logN)。通过 epoll_ctl 函数添加进来的 fd 都会被放在红黑树的某个节点内,所以,重复添加是没有用的。当把 fd 添加进来的时候时候会完成关键的一步:该 fd 都会与相应的设备(网卡)驱动程序建立回调关系,也就是在内核中断处理程序为它注册一个回调函数,在 fd 相应的事件触发(中断)之后(设备就绪了),内核就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback这个回调函数其实就是把这个 fd 添加到 rdllist 这个双向链表(就绪链表)中。epoll_wait 实际上就是去检查 rdlist 双向链表中是否有就绪的 fd,当 rdlist 为空(无就绪fd)时挂起当前进程,直到 rdlist 非空时进程才被唤醒并返回。

相比于 select&poll 调用时会将全部监听的 fd 从用户态空间拷贝至内核态空间并线性扫描一遍找出就绪的 fd 再返回到用户态,epoll_wait 则是直接返回已就绪 fd,因此 epoll 的 I/O 性能不会像 select&poll 那样随着监听的 fd 数量增加而出现线性衰减,是一个非常高效的 I/O 事件驱动技术。

由于使用 epoll 的 I/O 多路复用需要用户进程自己负责 I/O 读写,从用户进程的角度看,读写过程是阻塞的,所以 select&poll&epoll 本质上都是同步 I/O 模型,而像 Windows 的 IOCP 这一类的异步 I/O,只需要在调用 WSARecv 或 WSASend 方法读写数据的时候把用户空间的内存 buffer 提交给 kernel,kernel 负责数据在用户空间和内核空间拷贝,完成之后就会通知用户进程,整个过程不需要用户进程参与,所以是真正的异步 I/O。

延伸

另外,我看到有些文章说 epoll 之所以性能高是因为利用了 linux 的 mmap 内存映射让内核和用户进程共享了一片物理内存,用来存放就绪 fd 列表和它们的数据 buffer,所以用户进程在 epoll_wait返回之后用户进程就可以直接从共享内存那里读取/写入数据了,这让我很疑惑,因为首先看epoll_wait的函数声明:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

第二个参数:就绪事件列表,是需要在用户空间分配内存然后再传给epoll_wait的,如果内核会用 mmap 设置共享内存,直接传递一个指针进去就行了,根本不需要在用户态分配内存,多此一举。其次,内核和用户进程通过 mmap 共享内存是一件极度危险的事情,内核无法确定这块共享内存什么时候会被回收,而且这样也会赋予用户进程直接操作内核数据的权限和入口,非常容易出现大的系统漏洞,因此一般极少会这么做。所以我很怀疑 epoll 是不是真的在 linux kernel 里用了 mmap,我就去看了下最新版本(5.3.9)的 linux kernel 源码:

/*
* Implement the event wait interface for the eventpoll file. It is the kernel
* part of the user space epoll_wait(2).
*/

static int do_epoll_wait(int epfd, struct epoll_event __user *events,
int maxevents, int timeout)
{
// ...

/* Time to fish for events ... */
error = ep_poll(ep, events, maxevents, timeout);
}

// 如果 epoll_wait 入参时设定 timeout == 0, 那么直接通过 ep_events_available 判断当前是否有用户感兴趣的事件发生,如果有则通过 ep_send_events 进行处理
// 如果设置 timeout > 0,并且当前没有用户关注的事件发生,则进行休眠,并添加到 ep->wq 等待队列的头部;对等待事件描述符设置 WQ_FLAG_EXCLUSIVE 标志
// ep_poll 被事件唤醒后会重新检查是否有关注事件,如果对应的事件已经被抢走,那么 ep_poll 会继续休眠等待
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events, int maxevents, long timeout)
{
// ...

send_events:
/*
* Try to transfer events to user space. In case we get 0 events and
* there's still timeout left over, we go trying again in search of
* more luck.
*/


// 如果一切正常, 有 event 发生, 就开始准备数据 copy 给用户空间了
// 如果有就绪的事件发生,那么就调用 ep_send_events 将就绪的事件 copy 到用户态内存中,
// 然后返回到用户态,否则判断是否超时,如果没有超时就继续等待就绪事件发生,如果超时就返回用户态。
// 从 ep_poll 函数的实现可以看到,如果有就绪事件发生,则调用 ep_send_events 函数做进一步处理
if (!res && eavail &&
!(res = ep_send_events(ep, events, maxevents)) && !timed_out)
goto fetch_events;

// ...
}

// ep_send_events 函数是用来向用户空间拷贝就绪 fd 列表的,它将用户传入的就绪 fd 列表内存简单封装到
// ep_send_events_data 结构中,然后调用 ep_scan_ready_list 将就绪队列中的事件写入用户空间的内存;
// 用户进程就可以访问到这些数据进行处理
static int ep_send_events(struct eventpoll *ep,
struct epoll_event __user *events, int maxevents)
{
struct ep_send_events_data esed;

esed.maxevents = maxevents;
esed.events = events;
// 调用 ep_scan_ready_list 函数检查 epoll 实例 eventpoll 中的 rdllist 就绪链表,
// 并注册一个回调函数 ep_send_events_proc,如果有就绪 fd,则调用 ep_send_events_proc 进行处理
ep_scan_ready_list(ep, ep_send_events_proc, &esed, 0, false);
return esed.res;
}

// 调用 ep_scan_ready_list 的时候会传递指向 ep_send_events_proc 函数的函数指针作为回调函数,
// 一旦有就绪 fd,就会调用 ep_send_events_proc 函数
static __poll_t ep_send_events_proc(struct eventpoll *ep, struct list_head *head, void *priv)
{
// ...

/*
* If the event mask intersect the caller-requested one,
* deliver the event to userspace. Again, ep_scan_ready_list()
* is holding ep->mtx, so no operations coming from userspace
* can change the item.
*/

revents = ep_item_poll(epi, &pt, 1);
// 如果 revents 为 0,说明没有就绪的事件,跳过,否则就将就绪事件拷贝到用户态内存中
if (!revents)
continue;
// 将当前就绪的事件和用户进程传入的数据都通过 __put_user 拷贝回用户空间,
// 也就是调用 epoll_wait 之时用户进程传入的 fd 列表的内存
if (__put_user(revents, &uevent->events) || __put_user(epi->event.data, &uevent->data)) {
list_add(&epi->rdllink, head);
ep_pm_stay_awake(epi);
if (!esed->res)
esed->res = -EFAULT;
return 0;
}

// ...
}

do_epoll_wait开始层层跳转,我们可以很清楚地看到最后内核是通过__put_user函数把就绪 fd 列表和事件返回到用户空间,而__put_user正是内核用来拷贝数据到用户空间的标准函数。此外,我并没有在 linux kernel 的源码中和 epoll 相关的代码里找到 mmap 系统调用做内存映射的逻辑,所以基本可以得出结论:epoll 在 linux kernel 里并没有使用 mmap 来做用户空间和内核空间的内存共享,所以那些说 epoll 使用了 mmap 的文章都是误解。

Non-blocking I/O

什么叫非阻塞 I/O,顾名思义就是:所有 I/O 操作都是立刻返回而不会阻塞当前用户进程。I/O 多路复用通常情况下需要和非阻塞 I/O 搭配使用,否则可能会产生意想不到的问题。比如,epoll 的 ET(边缘触发) 模式下,如果不使用非阻塞 I/O,有极大的概率会导致阻塞 event-loop 线程,从而降低吞吐量,甚至导致 bug。

Linux 下,我们可以通过 fcntl 系统调用来设置 O_NONBLOCK标志位,从而把 socket 设置成 non-blocking。当对一个 non-blocking socket 执行读操作时,流程是这个样子:

当用户进程发出 read 操作时,如果 kernel 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个 EAGAIN error。从用户进程角度讲 ,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦 kernel 中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,non-blocking I/O 的特点是用户进程需要不断的主动询问 kernel 数据好了没有。

Go netpoll

一个典型的 Go TCP server:

package main

import (
"fmt"
"net"
)

func main() {
listen, err := net.Listen("tcp", ":8888")
if err != nil {
fmt.Println("listen error: ", err)
return
}

for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept error: ", err)
break
}

// start a new goroutine to handle the new connection
go HandleConn(conn)
}
}
func HandleConn(conn net.Conn) {
defer conn.Close()
packet := make([]byte, 1024)
for {
// 如果没有可读数据,也就是读 buffer 为空,则阻塞
_, _ = conn.Read(packet)
// 同理,不可写则阻塞
_, _ = conn.Write(packet)
}
}

上面是一个基于 Go 原生网络模型(基于 netpoll)编写的一个 TCP server,模式是 goroutine-per-connection,在这种模式下,开发者使用的是同步的模式去编写异步的逻辑而且对于开发者来说 I/O 是否阻塞是无感知的,也就是说开发者无需考虑 goroutines 甚至更底层的线程、进程的调度和上下文切换。而 Go netpoll 最底层的事件驱动技术肯定是基于 epoll/kqueue/iocp 这一类的 I/O 事件驱动技术,只不过是把这些调度和上下文切换的工作转移到了 runtime 的 Go scheduler,让它来负责调度 goroutines,从而极大地降低了程序员的心智负担!

Go netpoll 核心

Go netpoll 通过在底层对 epoll/kqueue/iocp 的封装,从而实现了使用同步编程模式达到异步执行的效果。总结来说,所有的网络操作都以网络描述符 netFD 为中心实现。netFD 与底层 PollDesc 结构绑定,当在一个 netFD 上读写遇到 EAGAIN 错误时,就将当前 goroutine 存储到这个 netFD 对应的 PollDesc 中,同时调用 gopark 把当前 goroutine 给 park 住,直到这个 netFD 上再次发生读写事件,才将此 goroutine 给 ready 激活重新运行。显然,在底层通知 goroutine 再次发生读写等事件的方式就是 epoll/kqueue/iocp 等事件驱动机制。

接下来我们通过分析最新的 Go 源码(v1.13.4),解读一下整个 netpoll 的运行流程。

上面的示例代码中相关的在源码里的几个数据结构和方法:

// TCPListener is a TCP network listener. Clients should typically
// use variables of type Listener instead of assuming TCP.
type TCPListener struct {
fd *netFD
lc ListenConfig
}

// Accept implements the Accept method in the Listener interface; it
// waits for the next call and returns a generic Conn.
func (l *TCPListener) Accept() (Conn, error) {
if !l.ok() {
return nil, syscall.EINVAL
}
c, err := l.accept()
if err != nil {
return nil, &OpError{Op: "accept", Net: l.fd.net, Source: nil, Addr: l.fd.laddr, Err: err}
}
return c, nil
}

func (ln *TCPListener) accept() (*TCPConn, error) {
fd, err := ln.fd.accept()
if err != nil {
return nil, err
}
tc := newTCPConn(fd)
if ln.lc.KeepAlive >= 0 {
setKeepAlive(fd, true)
ka := ln.lc.KeepAlive
if ln.lc.KeepAlive == 0 {
ka = defaultTCPKeepAlive
}
setKeepAlivePeriod(fd, ka)
}
return tc, nil
}

// TCPConn is an implementation of the Conn interface for TCP network
// connections.
type TCPConn struct {
conn
}

// Conn
type conn struct {
fd *netFD
}

type conn struct {
fd *netFD
}

func (c *conn) ok() bool { return c != nil && c.fd != nil }

// Implementation of the Conn interface.

// Read implements the Conn Read method.
func (c *conn) Read(b []byte) (int, error) {
if !c.ok() {
return 0, syscall.EINVAL
}
n, err := c.fd.Read(b)
if err != nil && err != io.EOF {
err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
}
return n, err
}

// Write implements the Conn Write method.
func (c *conn) Write(b []byte) (int, error) {
if !c.ok() {
return 0, syscall.EINVAL
}
n, err := c.fd.Write(b)
if err != nil {
err = &OpError{Op: "write", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
}
return n, err
}

netFD

net.Listen("tcp", ":8888") 方法返回了一个 TCPListener,它是一个实现了 net.Listener 接口的 struct,而通过 listen.Accept() 接收的新连接 TCPConn 则是一个实现了 net.Conn 接口的 struct,它内嵌了 net.conn struct。仔细阅读上面的源码可以发现,不管是 Listener 的 Accept 还是 Conn 的 Read/Write 方法,都是基于一个 netFD 的数据结构的操作,netFD 是一个网络描述符,类似于 Linux 的文件描述符的概念,netFD 中包含一个 poll.FD 数据结构,而 poll.FD 中包含两个重要的数据结构 Sysfd 和 pollDesc,前者是真正的系统文件描述符,后者对是底层事件驱动的封装,所有的读写超时等操作都是通过调用后者的对应方法实现的。

netFDpoll.FD的源码:

// Network file descriptor.
type netFD struct {
pfd poll.FD

// immutable until Close
family int
sotype int
isConnected bool // handshake completed or use of association with peer
net string
laddr Addr
raddr Addr
}

// FD is a file descriptor. The net and os packages use this type as a
// field of a larger type representing a network connection or OS file.
type FD struct {
// Lock sysfd and serialize access to Read and Write methods.
fdmu fdMutex

// System file descriptor. Immutable until Close.
Sysfd int

// I/O poller.
pd pollDesc

// Writev cache.
iovecs *[]syscall.Iovec

// Semaphore signaled when file is closed.
csema uint32

// Non-zero if this file has been set to blocking mode.
isBlocking uint32

// Whether this is a streaming descriptor, as opposed to a
// packet-based descriptor like a UDP socket. Immutable.
IsStream bool

// Whether a zero byte read indicates EOF. This is false for a
// message based socket connection.
ZeroReadIsEOF bool

// Whether this is a file rather than a network socket.
isFile bool
}

pollDesc

前面提到了 pollDesc 是底层事件驱动的封装,netFD 通过它来完成各种 I/O 相关的操作,它的定义如下:

type pollDesc struct {
runtimeCtx uintptr
}

这里的 struct 只包含了一个指针,而通过 pollDesc 的 init 方法,我们可以找到它具体的定义是在runtime.pollDesc这里:

func (pd *pollDesc) init(fd *FD) error {
serverInit.Do(runtime_pollServerInit)
ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd))
if errno != 0 {
if ctx != 0 {
runtime_pollUnblock(ctx)
runtime_pollClose(ctx)
}
return syscall.Errno(errno)
}
pd.runtimeCtx = ctx
return nil
}

// Network poller descriptor.
//
// No heap pointers.
//
//go:notinheap
type pollDesc struct {
link *pollDesc // in pollcache, protected by pollcache.lock

// The lock protects pollOpen, pollSetDeadline, pollUnblock and deadlineimpl operations.
// This fully covers seq, rt and wt variables. fd is constant throughout the PollDesc lifetime.
// pollReset, pollWait, pollWaitCanceled and runtime·netpollready (IO readiness notification)
// proceed w/o taking the lock. So closing, everr, rg, rd, wg and wd are manipulated
// in a lock-free way by all operations.
// NOTE(dvyukov): the following code uses uintptr to store *g (rg/wg),
// that will blow up when GC starts moving objects.
lock mutex // protects the following fields
fd uintptr
closing bool
everr bool // marks event scanning error happened
user uint32 // user settable cookie
rseq uintptr // protects from stale read timers
rg uintptr // pdReady, pdWait, G waiting for read or nil
rt timer // read deadline timer (set if rt.f != nil)
rd int64 // read deadline
wseq uintptr // protects from stale write timers
wg uintptr // pdReady, pdWait, G waiting for write or nil
wt timer // write deadline timer
wd int64 // write deadline
}

runtime.pollDesc包含自身类型的一个指针,用来保存下一个runtime.pollDesc的地址,以此来实现链表,可以减少数据结构的大小,所有的runtime.pollDesc保存在runtime.pollCache结构中,定义如下:

type pollCache struct {
lock mutex
first *pollDesc
// PollDesc objects must be type-stable,
// because we can get ready notification from epoll/kqueue
// after the descriptor is closed/reused.
// Stale notifications are detected using seq variable,
// seq is incremented when deadlines are changed or descriptor is reused.
}

net.Listen

调用 net.Listen之后,底层会通过 Linux 的系统调用socket 方法创建一个 fd 分配给 listener,并用以来初始化 listener 的 netFD,接着调用 netFD 的listenStream方法完成对 socket 的 bind&listen 操作以及对 netFD 的初始化(主要是对 netFD 里的 pollDesc 的初始化),相关源码如下:

// 调用 linux 系统调用 socket 创建 listener fd 并设置为为阻塞 I/O    
s, err := socketFunc(family, sotype|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, proto)
// On Linux the SOCK_NONBLOCK and SOCK_CLOEXEC flags were
// introduced in 2.6.27 kernel and on FreeBSD both flags were
// introduced in 10 kernel. If we get an EINVAL error on Linux
// or EPROTONOSUPPORT error on FreeBSD, fall back to using
// socket without them.

socketFunc func(int, int, int) (int, error) = syscall.Socket

// 用上面创建的 listener fd 初始化 listener netFD
if fd, err = newFD(s, family, sotype, net); err != nil {
poll.CloseFunc(s)
return nil, err
}

// 对 listener fd 进行 bind&listen 操作,并且调用 init 方法完成初始化
func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error {
// ...

// 完成绑定操作
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)
}

// 调用 init,内部会调用 poll.FD.Init,最后调用 pollDesc.init
if err = fd.init(); err != nil {
return err
}
lsa, _ = syscall.Getsockname(fd.pfd.Sysfd)
fd.setAddr(fd.addrFunc()(lsa), nil)
return nil
}

// 使用 sync.Once 来确保一个 listener 只持有一个 epoll 实例
var serverInit sync.Once

// netFD.init 会调用 poll.FD.Init 并最终调用到 pollDesc.init,
// 它会创建 epoll 实例并把 listener fd 加入监听队列
func (pd *pollDesc) init(fd *FD) error {
// runtime_pollServerInit 内部调用了 netpollinit 来创建 epoll 实例
serverInit.Do(runtime_pollServerInit)

// runtime_pollOpen 内部调用了 netpollopen 来将 listener fd 注册到
// epoll 实例中,另外,它会初始化一个 pollDesc 并返回
ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd))
if errno != 0 {
if ctx != 0 {
runtime_pollUnblock(ctx)
runtime_pollClose(ctx)
}
return syscall.Errno(errno)
}
// 把真正初始化完成的 pollDesc 实例赋值给当前的 pollDesc 代表自身的指针,
// 后续使用直接通过该指针操作
pd.runtimeCtx = ctx
return nil
}

// netpollopen 会被 runtime_pollOpen,注册 fd 到 epoll 实例,
// 同时会利用万能指针把 pollDesc 保存到 epollevent 的一个 8 位的字节数组 data 里
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)
}

我们前面提到的 epoll 的三个基本调用,Go 在源码里实现了对那三个调用的封装:

#include <sys/epoll.h>  
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

// Go 对上面三个调用的封装
func netpollinit()
func netpollopen(fd uintptr, pd *pollDesc) int32
func netpoll(block bool) gList

netFD 就是通过这三个封装来对 epoll 进行创建实例、注册 fd 和等待事件操作的。

Listener.Accept()

netpoll accept socket 的工作流程如下:

  1. 服务端的 netFD 在listen时会创建 epoll 的实例,并将 listenerFD 加入 epoll 的事件队列
  2. netFD 在accept时将返回的 connFD 也加入 epoll 的事件队列
  3. netFD 在读写时出现syscall.EAGAIN错误,通过 pollDesc 的 waitRead 方法将当前的 goroutine park 住,直到 ready,从 pollDesc 的waitRead中返回

Listener.Accept()接收来自客户端的新连接,具体还是调用netFD.accept方法来完成这个功能:

// Accept implements the Accept method in the Listener interface; it
// waits for the next call and returns a generic Conn.
func (l *TCPListener) Accept() (Conn, error) {
if !l.ok() {
return nil, syscall.EINVAL
}
c, err := l.accept()
if err != nil {
return nil, &OpError{Op: "accept", Net: l.fd.net, Source: nil, Addr: l.fd.laddr, Err: err}
}
return c, nil
}

func (ln *TCPListener) accept() (*TCPConn, error) {
fd, err := ln.fd.accept()
if err != nil {
return nil, err
}
tc := newTCPConn(fd)
if ln.lc.KeepAlive >= 0 {
setKeepAlive(fd, true)
ka := ln.lc.KeepAlive
if ln.lc.KeepAlive == 0 {
ka = defaultTCPKeepAlive
}
setKeepAlivePeriod(fd, ka)
}
return tc, nil
}

netFD.accept方法里再调用poll.FD.Accept,最后会使用 linux 的系统调用accept来完成新连接的接收,并且会把 accept 的 socket 设置成非阻塞 I/O 模式:

// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
if err := fd.readLock(); err != nil {
return -1, nil, "", err
}
defer fd.readUnlock()

if err := fd.pd.prepareRead(fd.isFile); err != nil {
return -1, nil, "", err
}
for {
// 使用 linux 系统调用 accept 接收新连接,创建对应的 socket
s, rsa, errcall, err := accept(fd.Sysfd)
// 因为 listener fd 在创建的时候已经设置成非阻塞的了,
// 所以 accept 方法会直接返回,不管有没有新连接到来;如果 err == nil 则表示正常建立新连接,直接返回
if err == nil {
return s, rsa, "", err
}
// 如果 err != nil,则判断 err == syscall.EAGAIN,符合条件则进入 pollDesc.waitRead 方法
switch err {
case syscall.EAGAIN:
if fd.pd.pollable() {
// 如果当前没有发生期待的 I/O 事件,那么 waitRead 会通过 park goroutine 让逻辑 block 在这里
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
}
}

// 使用 linux 的 accept 系统调用接收新连接并把这个 socket fd 设置成非阻塞 I/O
ns, sa, err := Accept4Func(s, syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC)
// On Linux the accept4 system call was introduced in 2.6.28
// kernel and on FreeBSD it was introduced in 10 kernel. If we
// get an ENOSYS error on both Linux and FreeBSD, or EINVAL
// error on Linux, fall back to using accept.

// Accept4Func is used to hook the accept4 call.
var Accept4Func func(int, int) (int, syscall.Sockaddr, error) = syscall.Accept4

pollDesc.waitRead方法主要负责检测当前这个 pollDesc 的上层 netFD 对应的 fd 是否有『期待的』I/O 事件发生,如果有就直接返回,否则就 park 住当前的 goroutine 并持续等待直至对应的 fd 上发生可读/可写或者其他『期待的』I/O 事件为止,然后它就会返回到外层的 for 循环,让 goroutine 继续执行逻辑。

Conn.Read/Conn.Write

我们先来看看Conn.Read方法是如何实现的,原理其实和 Listener.Accept 是一样的,具体调用链还是首先调用 conn 的netFD.Read,然后内部再调用 poll.FD.Read,最后使用 linux 的系统调用 read: syscall.Read完成数据读取:

// Implementation of the Conn interface.

// Read implements the Conn Read method.
func (c *conn) Read(b []byte) (int, error) {
if !c.ok() {
return 0, syscall.EINVAL
}
n, err := c.fd.Read(b)
if err != nil && err != io.EOF {
err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
}
return n, err
}

func (fd *netFD) Read(p []byte) (n int, err error) {
n, err = fd.pfd.Read(p)
runtime.KeepAlive(fd)
return n, wrapSyscallError("read", err)
}

// Read implements io.Reader.
func (fd *FD) Read(p []byte) (int, error) {
if err := fd.readLock(); err != nil {
return 0, err
}
defer fd.readUnlock()
if len(p) == 0 {
// If the caller wanted a zero byte read, return immediately
// without trying (but after acquiring the readLock).
// Otherwise syscall.Read returns 0, nil which looks like
// io.EOF.
// TODO(bradfitz): make it wait for readability? (Issue 15735)
return 0, nil
}
if err := fd.pd.prepareRead(fd.isFile); err != nil {
return 0, err
}
if fd.IsStream && len(p) > maxRW {
p = p[:maxRW]
}
for {
// 尝试从该 socket 读取数据,因为 socket 在被 listener accept 的时候设置成
// 了非阻塞 I/O,所以这里同样也是直接返回,不管有没有可读的数据
n, err := syscall.Read(fd.Sysfd, p)
if err != nil {
n = 0
// err == syscall.EAGAIN 表示当前没有期待的 I/O 事件发生,也就是 socket 不可读
if err == syscall.EAGAIN && fd.pd.pollable() {
// 如果当前没有发生期待的 I/O 事件,那么 waitRead
// 会通过 park goroutine 让逻辑 block 在这里
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}

// On MacOS we can see EINTR here if the user
// pressed ^Z. See issue #22838.
if runtime.GOOS == "darwin" && err == syscall.EINTR {
continue
}
}
err = fd.eofError(n, err)
return n, err
}
}

conn.Writeconn.Read的原理是一致的,它也是通过类似 pollDesc.waitReadpollDesc.waitWrite来 park 住 goroutine 直至期待的 I/O 事件发生才返回,而 pollDesc.waitWrite的内部实现原理和pollDesc.waitRead是一样的,都是基于runtime_pollWait,这里就不再赘述。

pollDesc.waitRead

pollDesc.waitRead内部调用了 runtime_pollWait来达成无 I/O 事件时 park 住 goroutine 的目的:

//go:linkname poll_runtime_pollWait internal/poll.runtime_pollWait
func poll_runtime_pollWait(pd *pollDesc, mode int) int {
err := netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}
// As for now only Solaris, illumos, and AIX use level-triggered IO.
if GOOS == "solaris" || GOOS == "illumos" || GOOS == "aix" {
netpollarm(pd, mode)
}
// 进入 netpollblock 并且判断是否有期待的 I/O 事件发生,
// 这里的 for 循环是为了一直等到 io ready
for !netpollblock(pd, int32(mode), false) {
err = netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}
// 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 0
}

// returns true if IO is ready, or false if timedout or closed
// waitio - wait only for completed IO, ignore errors
func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
// gpp 保存的是 goroutine 的数据结构 g,这里会根据 mode 的值决定是 rg 还是 wg
// 后面调用 gopark 之后,会把当前的 goroutine 的抽象数据结构 g 存入 gpp 这个指针
gpp := &pd.rg
if mode == 'w' {
gpp = &pd.wg
}

// set the gpp semaphore to WAIT
// 这个 for 循环是为了等待 io ready 或者 io wait
for {
old := *gpp
// gpp == pdReady 表示此时已有期待的 I/O 事件发生,
// 可以直接返回 unblock 当前 goroutine 并执行响应的 I/O 操作
if old == pdReady {
*gpp = 0
return true
}
if old != 0 {
throw("runtime: double wait")
}
// 如果没有期待的 I/O 事件发生,则通过原子操作把 gpp 的值置为 pdWait 并退出 for 循环
if atomic.Casuintptr(gpp, 0, pdWait) {
break
}
}

// need to recheck error states after setting gpp to WAIT
// this is necessary because runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl
// do the opposite: store to closing/rd/wd, membarrier, load of rg/wg

// waitio 此时是 false,netpollcheckerr 方法会检查当前 pollDesc 对应的 fd 是否是正常的,
// 通常来说 netpollcheckerr(pd, mode) == 0 是成立的,所以这里会执行 gopark
// 把当前 goroutine 给 park 住,直至对应的 fd 上发生可读/可写或者其他『期待的』I/O 事件为止,
// 然后 unpark 返回,在 gopark 内部会把当前 goroutine 的抽象数据结构 g 存入
// gpp(pollDesc.rg/pollDesc.wg) 指针里,以便在后面的 netpoll 函数取出 pollDesc 之后,
// 把 g 添加到链表里返回,然后重新调度运行该 goroutine
if waitio || netpollcheckerr(pd, mode) == 0 {
// 注册 netpollblockcommit 回调给 gopark,在 gopark 内部会执行它,保存当前 goroutine 到 gpp
gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceEvGoBlockNet, 5)
}
// be careful to not lose concurrent READY notification
old := atomic.Xchguintptr(gpp, 0)
if old > pdWait {
throw("runtime: corrupted polldesc")
}
return old == pdReady
}

// gopark 会停住当前的 goroutine 并且调用传递进来的回调函数 unlockf,从上面的源码我们可以知道这个函数是
// netpollblockcommit
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
if reason != waitReasonSleep {
checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
}
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
if status != _Grunning && status != _Gscanrunning {
throw("gopark: bad g status")
}
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason
mp.waittraceev = traceEv
mp.waittraceskip = traceskip
releasem(mp)
// can't do anything that might move the G between Ms here.
// gopark 最终会调用 park_m,在这个函数内部会调用 unlockf,也就是 netpollblockcommit,
// 然后会把当前的 goroutine,也就是 g 数据结构保存到 pollDesc 的 rg 或者 wg 指针里
mcall(park_m)
}

// park continuation on g0.
func park_m(gp *g) {
_g_ := getg()

if trace.enabled {
traceGoPark(_g_.m.waittraceev, _g_.m.waittraceskip)
}

casgstatus(gp, _Grunning, _Gwaiting)
dropg()

if fn := _g_.m.waitunlockf; fn != nil {
// 调用 netpollblockcommit,把当前的 goroutine,
// 也就是 g 数据结构保存到 pollDesc 的 rg 或者 wg 指针里
ok := fn(gp, _g_.m.waitlock)
_g_.m.waitunlockf = nil
_g_.m.waitlock = nil
if !ok {
if trace.enabled {
traceGoUnpark(gp, 2)
}
casgstatus(gp, _Gwaiting, _Grunnable)
execute(gp, true) // Schedule it back, never returns.
}
}
schedule()
}

// netpollblockcommit 在 gopark 函数里被调用
func netpollblockcommit(gp *g, gpp unsafe.Pointer) bool {
// 通过原子操作把当前 goroutine 抽象的数据结构 g,也就是这里的参数 gp 存入 gpp 指针,
// 此时 gpp 的值是 pollDesc 的 rg 或者 wg 指针
r := atomic.Casuintptr((*uintptr)(gpp), pdWait, uintptr(unsafe.Pointer(gp)))
if r {
// Bump the count of goroutines waiting for the poller.
// The scheduler uses this to decide whether to block
// waiting for the poller if there is nothing else to do.
atomic.Xadd(&netpollWaiters, 1)
}
return r
}

netpoll

前面已经从源码的角度分析完了 netpoll 是如何通过 park goroutine 从而达到阻塞 Accept/Read/Write 的效果,而通过调用 gopark,goroutine 会被放置在某个等待队列中(如 channel 的 waitq ,此时 G 的状态由_Grunning_Gwaitting),因此G必须被手动唤醒(通过 goready ),否则会丢失任务,应用层阻塞通常使用这种方式。

所以,最后还有一个非常关键的问题是:当 I/O 事件发生之后,netpoll 是通过什么方式唤醒那些在 I/O wait 的 goroutine 的?答案是通过 epoll_wait,在 Go 源码中的 src/runtime/netpoll_epoll.go文件中有一个 func netpoll(block bool) gList 方法,它会内部调用epoll_wait获取就绪的 fd 列表,并将每个 fd 对应的 goroutine 添加到链表返回

// polls for ready network connections
// returns list of goroutines that become runnable
func netpoll(block bool) gList {
if epfd == -1 {
return gList{}
}
waitms := int32(-1)
// 是否以阻塞模式调用 epoll_wait
if !block {
waitms = 0
}
var events [128]epollevent
retry:
// 获取就绪的 fd 列表
n := epollwait(epfd, &events[0], int32(len(events)), waitms)
if n < 0 {
if n != -_EINTR {
println("runtime: epollwait on fd", epfd, "failed with", -n)
throw("runtime: netpoll failed")
}
goto retry
}
// toRun 是一个 g 的链表,存储要恢复的 goroutines,最后返回给调用方
var toRun gList
for i := int32(0); i < n; i++ {
ev := &events[i]
if ev.events == 0 {
continue
}
var mode int32
// 判断发生的事件类型,读类型或者写类型
if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {
mode += 'r'
}
if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {
mode += 'w'
}
if mode != 0 {
// 取出保存在 epollevent 里的 pollDesc
pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
pd.everr = false
if ev.events == _EPOLLERR {
pd.everr = true
}
// 调用 netpollready,传入就绪 fd 的 pollDesc,把 fd 对应的 goroutine 添加到链表 toRun 中
netpollready(&toRun, pd, mode)
}
}
if block && toRun.empty() {
goto retry
}
return toRun
}

// netpollready 调用 netpollunblock 返回就绪 fd 对应的 goroutine 的抽象数据结构 g
func netpollready(toRun *gList, pd *pollDesc, mode int32) {
var rg, wg *g
if mode == 'r' || mode == 'r'+'w' {
rg = netpollunblock(pd, 'r', true)
}
if mode == 'w' || mode == 'r'+'w' {
wg = netpollunblock(pd, 'w', true)
}
if rg != nil {
toRun.push(rg)
}
if wg != nil {
toRun.push(wg)
}
}

// netpollunblock 会依据传入的 mode 决定从 pollDesc 的 rg 或者 wg 取出当时 gopark 之时存入的
// goroutine 抽象数据结构 g 并返回
func netpollunblock(pd *pollDesc, mode int32, ioready bool) *g {
// mode == 'r' 代表当时 gopark 是为了等待读事件,而 mode == 'w' 则代表是等待写事件
gpp := &pd.rg
if mode == 'w' {
gpp = &pd.wg
}

for {
// 取出 gpp 存储的 g
old := *gpp
if old == pdReady {
return nil
}
if old == 0 && !ioready {
// Only set READY for ioready. runtime_pollWait
// will check for timeout/cancel before waiting.
return nil
}
var new uintptr
if ioready {
new = pdReady
}
// 重置 pollDesc 的 rg 或者 wg
if atomic.Casuintpt

[ gev ] Go 语言优雅处理 TCP “粘包”

文章分享惜朝 发表了文章 • 0 个评论 • 620 次浏览 • 2019-11-01 10:48 • 来自相关话题

https://github.com/Allenxuxu/gev ...查看全部

https://github.com/Allenxuxu/gev

gev 是一个轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库,支持自定义协议,轻松快速搭建高性能服务器。

TCP 为什么会粘包


TCP 本身就是面向流的协议,就是一串没有界限的数据。所以本质上来说 TCP 粘包是一个伪命题。

TCP 底层并不关心上层业务数据,会套接字缓冲区的实际情况进行包的划分,一个完整的业务数据可能会被拆分成多次进行发送,也可能会将多个小的业务数据封装成一个大的数据包发送(Nagle算法)。

gev 如何优雅处理


gev 通过回调函数 OnMessage 通知用户数据到来,回调函数中会将用户数据缓冲区(ringbuffer)通过参数传递过来。

用户通过对 ringbuffer 操作,来进行数据解包,获取到完整用户数据后再进行业务操作。这样又一个明显的缺点,就是会让业务操作和自定义协议解析代码堆在一起。

所以,最近对 gev 进行了一次较大改动,主要是为了能够以插件的形式支持各种自定义的数据协议,让使用者可以便捷处理 TCP 粘包问题,专注于业务逻辑。



做法如下,定义一个接口 Protocol

```go
// Protocol 自定义协议编解码接口
type Protocol interface {
UnPacket(c *Connection, buffer *ringbuffer.RingBuffer) (interface{}, []byte)
Packet(c *Connection, data []byte) []byte
}
```


用户只需实现这个接口,并注册到 server 中,当客户端数据到来时,gev 会首先调用 UnPacket 方法,如果缓冲区中的数据足够组成一帧,则将数据解包,并返回真正的用户数据,然后在回调 OnMessage 函数并将数据通过参数传递。

下面,我们实现一个简单的自定义协议插件,来启动一个 Server :

```text
| 数据长度 n | payload |
| 4字节 | n 字节 |
```

```go
// protocol.go
package main

import (
"encoding/binary"
"github.com/Allenxuxu/gev/connection"
"github.com/Allenxuxu/ringbuffer"
"github.com/gobwas/pool/pbytes"
)

const exampleHeaderLen = 4

type ExampleProtocol struct{}

func (d *ExampleProtocol) UnPacket(c *connection.Connection, buffer *ringbuffer.RingBuffer) (interface{}, []byte) {
if buffer.VirtualLength() > exampleHeaderLen {
buf := pbytes.GetLen(exampleHeaderLen)
defer pbytes.Put(buf)
_, _ = buffer.VirtualRead(buf)
dataLen := binary.BigEndian.Uint32(buf)

if buffer.VirtualLength() >= int(dataLen) {
ret := make([]byte, dataLen)
_, _ = buffer.VirtualRead(ret)

buffer.VirtualFlush()
return nil, ret
} else {
buffer.VirtualRevert()
}
}
return nil, nil
}

func (d *ExampleProtocol) Packet(c *connection.Connection, data []byte) []byte {
dataLen := len(data)
ret := make([]byte, exampleHeaderLen+dataLen)
binary.BigEndian.PutUint32(ret, uint32(dataLen))
copy(ret[4:], data)
return ret
}
```

```go
// server.go
package main

import (
"flag"
"log"
"strconv"

"github.com/Allenxuxu/gev"
"github.com/Allenxuxu/gev/connection"
)

type example struct{}

func (s *example) OnConnect(c *connection.Connection) {
log.Println(" OnConnect : ", c.PeerAddr())
}
func (s *example) OnMessage(c *connection.Connection, ctx interface{}, data []byte) (out []byte) {
log.Println("OnMessage:", data)
out = data
return
}

func (s *example) OnClose(c *connection.Connection) {
log.Println("OnClose")
}

func main() {
handler := new(example)
var port int
var loops int

flag.IntVar(&port, "port", 1833, "server port")
flag.IntVar(&loops, "loops", -1, "num loops")
flag.Parse()

s, err := gev.NewServer(handler,
gev.Address(":"+strconv.Itoa(port)),
gev.NumLoops(loops),
gev.Protocol(&ExampleProtocol{}))
if err != nil {
panic(err)
}

log.Println("server start")
s.Start()
}
```
完整代码地址

当回调 `OnMessage` 函数的时候,会通过参数传递已经拆好包的用户数据。

当我们需要使用其他协议时,仅仅需要实现一个 Protocol 插件,然后只要 `gev.NewServer` 时指定即可:

```go
gev.NewServer(handler, gev.NumLoops(2), gev.Protocol(&XXXProtocol{}))
```

## 基于 Protocol Plugins 模式为 gev 实现 WebSocket 插件

得益于 Protocol Plugins 模式的引进,我可以将 WebSocket 的实现做成一个插件(WebSocket 协议构建在 TCP 之上),独立于 gev 之外。

```go
package websocket

import (
"log"

"github.com/Allenxuxu/gev/connection"
"github.com/Allenxuxu/gev/plugins/websocket/ws"
"github.com/Allenxuxu/ringbuffer"
)

// Protocol websocket
type Protocol struct {
upgrade *ws.Upgrader
}

// New 创建 websocket Protocol
func New(u *ws.Upgrader) *Protocol {
return &Protocol{upgrade: u}
}

// UnPacket 解析 websocket 协议,返回 header ,payload
func (p *Protocol) UnPacket(c *connection.Connection, buffer *ringbuffer.RingBuffer) (ctx interface{}, out []byte) {
upgraded := c.Context()
if upgraded == nil {
var err error
out, _, err = p.upgrade.Upgrade(buffer)
if err != nil {
log.Println("Websocket Upgrade :", err)
return
}
c.SetContext(true)
} else {
header, err := ws.VirtualReadHeader(buffer)
if err != nil {
log.Println(err)
return
}
if buffer.VirtualLength() >= int(header.Length) {
buffer.VirtualFlush()

payload := make([]byte, int(header.Length))
_, _ = buffer.Read(payload)

if header.Masked {
ws.Cipher(payload, header.Mask, 0)
}

ctx = &header
out = payload
} else {
buffer.VirtualRevert()
}
}
return
}

// Packet 直接返回
func (p *Protocol) Packet(c *connection.Connection, data []byte) []byte {
return data
}
```

具体的实现,可以到仓库的 [plugins/websocket](https://github.com/Allenxuxu/gev/tree/master/plugins/websocket) 查看。

## 相关文章

- [开源 gev: Go 实现基于 Reactor 模式的非阻塞 TCP 网络库](https://note.mogutou.xyz/articles/2019/09/19/1568896693634.html)
- [Go 网络库并发吞吐量测试](https://note.mogutou.xyz/articles/2019/09/22/1569146969662.html)

## 项目地址

https://github.com/Allenxuxu/gev

[开源] gev (支持 websocket 啦): Go 实现基于 Reactor 模式的非阻塞网络库

开源程序惜朝 发表了文章 • 0 个评论 • 352 次浏览 • 2019-10-24 11:04 • 来自相关话题

https://github.com/Allenxuxu/gev[gev](https://github.com/Allenxuxu/gev) ...查看全部

https://github.com/Allenxuxu/gev

[gev](https://github.com/Allenxuxu/gev) 是一个轻量、快速、高性能的基于 Reactor 模式的非阻塞网络库,底层并不使用 golang net 库,而是使用 epoll 和 kqueue。

现在它支持 WebSocket 啦!

支持定时任务,延时任务!

⬇️⬇️⬇️

## 特点

- 基于 epoll 和 kqueue 实现的高性能事件循环
- 支持多核多线程
- 动态扩容 Ring Buffer 实现的读写缓冲区
- 异步读写
- SO_REUSEPORT 端口重用支持
- 支持 WebSocket
- 支持定时任务,延时任务

## 性能测试

> 测试环境 Ubuntu18.04
- gev
- gnet
- eviop
- evio
- net (标准库)

### 吞吐量测试




仓库地址: https://github.com/Allenxuxu/gev

[gev] 一个轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库

Go开源项目惜朝 发表了文章 • 0 个评论 • 356 次浏览 • 2019-09-25 16:45 • 来自相关话题

`gev` 是一个 ...查看全部

`gev` 是一个轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库。

➡️➡️ https://github.com/Allenxuxu/gev

特点

- 基于 epoll 和 kqueue 实现的高性能事件循环
- 支持多核多线程
- 动态扩容 Ring Buffer 实现的读写缓冲区
- 异步读写
- SO_REUSEPORT 端口重用支持

网络模型

`gev` 只使用极少的 goroutine, 一个 goroutine 负责监听客户端连接,其他 goroutine (work 协程)负责处理已连接客户端的读写事件,work 协程数量可以配置,默认与运行主机 CPU 数量相同。

性能测试

> 测试环境 Ubuntu18.04 | 4 Virtual CPUs | 4.0 GiB

吞吐量测试

限制 GOMAXPROCS=1(单线程),1 个 work 协程


限制 GOMAXPROCS=4,4 个 work 协程


其他测试

速度测试


和同类库的简单性能比较, 压测方式与 evio 项目相同。
- gnet
- eviop
- evio
- net (标准库)

限制 GOMAXPROCS=1,1 个 work 协程


限制 GOMAXPROCS=1,4 个 work 协程


限制 GOMAXPROCS=4,4 个 work 协程


安装 gev


```bash
go get -u github.com/Allenxuxu/gev
```

快速入门


```go
package main

import (
"log"

"github.com/Allenxuxu/gev"
"github.com/Allenxuxu/gev/connection"
"github.com/Allenxuxu/ringbuffer"
)

type example struct{}

func (s *example) OnConnect(c *connection.Connection) {
log.Println(" OnConnect : ", c.PeerAddr())
}

func (s *example) OnMessage(c *connection.Connection, buffer *ringbuffer.RingBuffer) (out []byte) {
log.Println("OnMessage")
first, end := buffer.PeekAll()
out = first
if len(end) > 0 {
out = append(out, end...)
}
buffer.RetrieveAll()
return
}

func (s *example) OnClose(c *connection.Connection) {
log.Println("OnClose")
}

func main() {
handler := new(example)

s, err := gev.NewServer(handler,
gev.Address(":1833"),
gev.NumLoops(2),
gev.ReusePort(true))
if err != nil {
panic(err)
}

s.Start()
}
```

Handler 是一个接口,我们的程序必须实现它。

```go
type Handler interface {
OnConnect(c *connection.Connection)
OnMessage(c *connection.Connection, buffer *ringbuffer.RingBuffer) []byte
OnClose(c *connection.Connection)
}

func NewServer(handler Handler, opts ...Option) (server *Server, err error) {
```

在消息到来时,gev 会回调 OnMessage ,在这个函数中可以通过返回一个切片来发送数据给客户端。

```go
func (s *example) OnMessage(c *connection.Connection, buffer *ringbuffer.RingBuffer) (out []byte)
```

Connection 还提供 Send 方法来发送数据。Send 并不会立刻发送数据,而是先添加到 event loop 的任务队列中,然后唤醒 event loop 去发送。

更详细的使用方式可以参考示例:[服务端定时推送]

```go
func (c *Connection) Send(buffer []byte) error
```

Connection ShutdownWrite 会关闭写端,从而断开连接。

更详细的使用方式可以参考示例:[限制最大连接数]

```go
func (c *Connection) ShutdownWrite() error
```


➡️➡️ https://github.com/Allenxuxu/gev



微软、IBM、GitLab 等大厂全部到齐的 OCS 第一天有什么看点?

文章分享NebulaGraph 发表了文章 • 0 个评论 • 739 次浏览 • 2019-09-19 11:24 • 来自相关话题

在本周一的[推文](https://mp.weixin.qq.com/s/vLnfwiqgPlhvf_O7ixPQTg)中我们大致介绍了下 Open Core 峰会及到场嘉宾,(≧▽≦) 当然还有 Nebula Graph 在会场的展位位置图,本文我们来看看 ...查看全部
在本周一的[推文](https://mp.weixin.qq.com/s/vLnfwiqgPlhvf_O7ixPQTg)中我们大致介绍了下 Open Core 峰会及到场嘉宾,(≧▽≦) 当然还有 Nebula Graph 在会场的展位位置图,本文我们来看看 Open Core 峰会第一天有哪些值得一看的议题。

本文目录

- Adventures and Misadventures in Category Creation & OSS: The Neo4j Story - Emil Eifrem, Neo4j, Inc.
- [Creating Authentic Value: Open Source vs. Open Core - Deborah Bryant, Red Hat, Inc.](https://opencoresummit2019.sched.com/event/UNK9/creating-authentic-value-open-source-vs-open-core-deborah-bryant-red-hat-inc)
- [Commercial Open Source Business Models - In the Age of Hyper-Clouds, GitLab bets on Buyer-based Open Core - Priyanka Sharma, GitLab Inc.](https://opencoresummit2019.sched.com/event/UNKC/commercial-open-source-business-models-in-the-age-of-hyper-clouds-gitlab-bets-on-buyer-based-open-core-priyanka-sharma-gitlab-inc)
- [Opening up the cloud with Crossplane - Bassam Tabbara, Upbound](https://opencoresummit2019.sched.com/event/UNKF/opening-up-the-cloud-with-crossplane-bassam-tabbara-upbound)
- [Decentralization: A new opportunity for open source monetization - Ben Golub, Storj Labs](https://opencoresummit2019.sched.com/event/UNKI/decentralization-a-new-opportunity-for-open-source-monetization-ben-golub-storj-labs)
- [Open Source Adoption: The Pitfalls and Victories - Ido Green, JFrog](https://opencoresummit2019.sched.com/event/UNKO/open-source-adoption-the-pitfalls-and-victories-ido-green-jfrog)
- [On building a business around viable open-source project - Kohsuke Kawaguchi, CloudBees, Inc.Kohsuke Kawaguchi](https://opencoresummit2019.sched.com/event/UNKU/on-building-a-business-around-viable-open-source-project-kohsuke-kawaguchi-cloudbees-inc)
- [Your Product and Your Project are Different - Sarah Novotny, Microsoft](https://opencoresummit2019.sched.com/event/UNKX/your-product-and-your-project-are-different-sarah-novotny-microsoft)
- The open source journey from Initical code to IPO - Shay Banon, Elasticsearch creator/founder
- [PANEL - Investing in Open Source - From Promising Project to Enduring Company - Konstantine Buhler, Meritech Capital (Moderator)](https://opencoresummit2019.sched.com/event/UNKm/panel-investing-in-open-source-from-promising-project-to-enduring-company-konstantine-buhler-meritech-capital-moderator)
- [Percona - Monetizing Open Source without Open Core - Peter Zaitsev, Percona](https://opencoresummit2019.sched.com/event/UNKs/percona-monetizing-open-source-without-open-core-peter-zaitsev-percona)
- [Cygnus: COSS From the Absolute Beginning - David Henkel-Wallace, Leela.ai](https://opencoresummit2019.sched.com/event/UNKv/cygnus-coss-from-the-absolute-beginning-david-henkel-wallace-leelaai)
- [Software-Defined Telecom: From Open Source to Mainstream, Bringing Complex Technology to the Masses - Anthony Minessale, SignalWire INC](https://opencoresummit2019.sched.com/event/UNKy/software-defined-telecom-from-open-source-to-mainstream-bringing-complex-technology-to-the-masses-anthony-minessale-signalwire-inc)
- [Open Source and Open Core – Not a Zero Sum Game - Andi Gutmans, AWS](https://opencoresummit2019.sched.com/event/UNL1/open-source-and-open-core-not-a-zero-sum-game-andi-gutmans-aws)
- [Dual licensing: its place in an Open Source business, with reflections on a 32-year success story - L Peter Deutsch, Artifex Software](https://opencoresummit2019.sched.com/event/UNL4/dual-licensing-its-place-in-an-open-source-business-with-reflections-on-a-32-year-success-story-l-peter-deutsch-artifex-software)
- [Disrupting the Enterprise with Open Source - Marco Palladino, Kong](https://opencoresummit2019.sched.com/event/UNL7/disrupting-the-enterprise-with-open-source-marco-palladino-kong)
- [The New Business Model - Creating Operational Excellence around Open Source with Cloud - Jason McGee, IBM](https://opencoresummit2019.sched.com/event/UNLA/the-new-business-model-creating-operational-excellence-around-open-source-with-cloud-jason-mcgee-ibm)
- [Going Global with Open Core - John Newton, Alfresco Software](https://opencoresummit2019.sched.com/event/UNLD/going-global-with-open-core-john-newton-alfresco-software)
- [Making Money with Open Source - Marten Mickos, HackerOne](https://opencoresummit2019.sched.com/event/UkwM/making-money-with-open-source-marten-mickos-hackerone)

![](https://oscimg.oschina.net/oscnet/fc09fe47f578400bb0ebde6ff5c4da36f0f.jpg)

### [Adventures and Misadventures in Category Creation & OSS: The Neo4j Story - Emil Eifrem, Neo4j, Inc.](https://opencoresummit2019.sched.com/event/UNK6/adventures-and-misadventures-in-category-creation-oss-the-neo4j-story-emil-eifrem-neo4j-inc)

Neo4j Founder and CEO Emil Eifrem will share war stories from a journey that began with sketching out the property graph data model on a napkin, and has led to running the leading company in the fastest growing database category.

All the while trying (and sometimes succeeding!) to combine category creation, open source, developer marketing and enterprise selling into a complex brew that will one day lead to inevitable world domination.

> 不知道作为现图数据库领域的领头羊,Neo4j 除了给我们科普构建图模型之外,会给我们带来怎么样的惊喜…

![](https://oscimg.oschina.net/oscnet/1f0360039496d902ae4b44d2e2a5a91dca5.jpg)

### [Creating Authentic Value: Open Source vs. Open Core - Deborah Bryant, Red Hat, Inc.](https://opencoresummit2019.sched.com/event/UNK9/creating-authentic-value-open-source-vs-open-core-deborah-bryant-red-hat-inc)

Recent emphasis on cloud technologies has put a spotlight on how software companies work in today’s business and technical environments. Some companies have moved from traditional software licenses and instead have chosen to try to protect their software through creative licenses such as “open core”. Unlike open source, where value is placed on community, collaboration, and services, open core businesses place their value on software features.

Red Hat’s successful experience as a completely open source company has shown that value is not in the code, but in the support and expertise by being a part of a true community. In this talk, Red Hat’s Deb Bryant will share observations and cautionary tales from the world’s most successful open source company on how open core has time and again been demonstrated to not be truly open, limits community innovation, and delivers essentially proprietary software to customers.

> Red Hat 本次带来的演讲主要是和开源许可证有关,不禁让人想起去年 Redis 和 Neo4j 都更改了开源许可证的事情,希望本次 Red Hat 的分享能解答我们对开放源码和开放软件核心业务的部分疑惑…

![](https://oscimg.oschina.net/oscnet/a3e9635a14d4ac173300e880b369d87836a.jpg)

### [Commercial Open Source Business Models - In the Age of Hyper-Clouds, GitLab bets on Buyer-based Open Core - Priyanka Sharma, GitLab Inc.](https://opencoresummit2019.sched.com/event/UNKC/commercial-open-source-business-models-in-the-age-of-hyper-clouds-gitlab-bets-on-buyer-based-open-core-priyanka-sharma-gitlab-inc)

Today is the day of hyper clouds and companies based on open source projects have challenges building a business. GitLab, the first single application for the DevSecOps lifecycle, has grown 177% YoY at the same time. In this talk, Priyanka Sharma, Director of Technical Evangelism, GitLab Inc. will share the road the company took to success. She will talk about:

- Implications of being a commercial open source (COSS) company today
- The business models GitLab considered
- Our chosen model of buyer based open core
- How buyer based open core works
- Advantages of our choice

This talk is ideal for anyone looking to understand how open source can be monetized and will provide unique insight whether you are a startup founder, end user, or cloud provider.

> GitLab 的演讲主要是如何将开源和商业化相结合,开源本身并不是一种商业模式,而是一种开发模式和软件的推广,或者说是传播模式,GitLab 本次的演讲也许能给我们一些开源同商业相结合的新启发。

![](https://oscimg.oschina.net/oscnet/a6f7efdd4c8ca056c8bc573490bb0bd8944.jpg)

### [Opening up the cloud with Crossplane - Bassam Tabbara, Upbound](https://opencoresummit2019.sched.com/event/UNKF/opening-up-the-cloud-with-crossplane-bassam-tabbara-upbound)

Today, cloud computing is dominated by a few vertically integrated commercial providers. In this talk Bassam Tabbara, founder of the open source Crossplane project and CEO of Upbound.io, will discuss how the open source community can tip the market towards a more open, horizontally-integrated cloud ecosystem.

![](https://oscimg.oschina.net/oscnet/b09dd636e0ca057d62d474e8d99c9e8df1e.jpg)

### [Decentralization: A new opportunity for open source monetization - Ben Golub, Storj Labs](https://opencoresummit2019.sched.com/event/UNKI/decentralization-a-new-opportunity-for-open-source-monetization-ben-golub-storj-labs)

The emergence of cloud computing has spurred massive innovation, decoupling software from the infrastructure on which it runs. However it has also brought about huge challenges for open source companies in search of sustainable business models, causing them incorporate new licensing models other tactics to compete against the cloud computing giants. Decentralized cloud platforms make new economic models possible that allow open source platforms to monetize all their customers - not just their enterprise users.

This session will look at the intersection of open source, decentralization, and cloud and how it can empower open source platforms in new ways. It will also explore how decentralization and open source software can combine to eliminate downtime in the cloud, remove single points of failure, democratize trust, improve accountability, and radically improve security and privacy at a foundational level.

> 云计算将软件同其运行环境相分离,Storj Las 带来的演讲侧重点在于开放源码、分散化和云的相结合实践方案,此外你对云计算中的安全、隐私方面有兴趣的,这个主题或许能给你带来新的领悟。

![](https://oscimg.oschina.net/oscnet/315c8b6844089cb88c1a239c595b3cb1710.jpg)

### [Open Source Adoption: The Pitfalls and Victories - Ido Green, JFrog](https://opencoresummit2019.sched.com/event/UNKO/open-source-adoption-the-pitfalls-and-victories-ido-green-jfrog)

In this talk, we’ll share some key insights so other project owners can avoid falling into the same holes we’ve fallen into. Further, we’ll share some interesting statistics about the DevOps market that will help you gain insight into your own domain, and how you can practically address larger market movements that the bosses’ bosses’ bosses are really caring about.

> 看样子,JFrog 这次的会给我们带来大量的 DevOps 数据方面的分享,不知他们在市场上踩过的坑能给我们带来多少启发:)

![](https://oscimg.oschina.net/oscnet/a45dc8e270a591ad5292060ec2cc62e68bd.jpg)

### [On building a business around viable open-source project - Kohsuke Kawaguchi, CloudBees, Inc.Kohsuke Kawaguchi](https://opencoresummit2019.sched.com/event/UNKU/on-building-a-business-around-viable-open-source-project-kohsuke-kawaguchi-cloudbees-inc)

In this talk, I’d like to look back at the history and share what worked and what didn’t work, such as the difficulty of justifying engineering efforts to OSS, how enterprise product can stifle open-source, and the impact and the consequences of hiring people from the comm

> CloudBees 的演讲摘要中规中矩,不知周四的现场演讲会不会带来不一样的体验…

![](https://oscimg.oschina.net/oscnet/bbc3b25cc874adb838982f2e432cbd5ce49.jpg)

### [Your Product and Your Project are Different - Sarah Novotny, Microsoft](https://opencoresummit2019.sched.com/event/UNKX/your-product-and-your-project-are-different-sarah-novotny-microsoft)

Open source is a licensing and development model. Your open source project may be the basis for your product, but they are two different things. Let’s talk about a few challenges which can happen if that distinction is forgotten.

![](https://oscimg.oschina.net/oscnet/15a05aba1d16cc033cc24eefc22e6a35fa4.jpg)

### [The open source journey from Initical code to IPO - Shay Banon, Elasticsearch creator/founder](https://opencoresummit2019.sched.com/event/UNKa)

Elasticsearch creator/founder and Elastic CEO shares first-hand experiences writing the first million lines of code (likely more!) and guiding the company to an IPO eight years later.

![](https://oscimg.oschina.net/oscnet/dc0fbc0f35641e8a4c36700694de24523ae.jpg)

### [PANEL - Investing in Open Source - From Promising Project to Enduring Company - Konstantine Buhler, Meritech Capital (Moderator)](https://opencoresummit2019.sched.com/event/UNKm/panel-investing-in-open-source-from-promising-project-to-enduring-company-konstantine-buhler-meritech-capital-moderator)

Join two of the world's top open source investors, Martin Casado (Andreessen Horowitz) and Mike Volpi (Index Ventures) as they discuss open source investing. Every open source company begins with a passionate community. But the most enduring open source companies navigate a nuanced path to commercialization. Don't miss out as Martin and Mike share stories, discuss patterns, and offer insight into building incredible open source companies. The panel will be moderated by Konstantine Buhler (Meritech Capital).

> "每个开源公司都始于一个充满激情的社区", 也许 Martin 和 Mike 的故事能给做开源社区的公司指一条明路…

![](https://oscimg.oschina.net/oscnet/59e02b297571758454485715ea1692c0253.jpg)

### [Percona - Monetizing Open Source without Open Core - Peter Zaitsev, Percona](https://opencoresummit2019.sched.com/event/UNKs/percona-monetizing-open-source-without-open-core-peter-zaitsev-percona)

All Percona Software Products are 100% Free and Open Source. We do not do Open Core, Shared Source or Open Source Eventually. Yet Percona has been growing every one of 13 years it has been in existence, all without relying on Venture Capital or other External Funding, and now reaching over $20M of ARR. In this presentation you will hear our story and why our approach to business may (or may not) workfor you.

> 开源数据库技术大会主办方 Percona 好像和 Open Core 峰会唱了个反调:without Open Core :) ,也许我们也能学习下如何通过开源和软件,且不靠融资,获得 2 千美元的 ARR

![](https://oscimg.oschina.net/oscnet/08db7c419f5c3238ee24bf669040df0d3bf.jpg)

### [Cygnus: COSS From the Absolute Beginning - David Henkel-Wallace, Leela.ai](https://opencoresummit2019.sched.com/event/UNKv/cygnus-coss-from-the-absolute-beginning-david-henkel-wallace-leelaai)

In this talk I will briefly describe the software ecosystem of 1989 and what inspired me to found Cygnus, along with Michael Tiemann and John Gilmore, in my living room. I will then discuss what has changed and what has remained the same since then. Finally, I will address some risks I see for the COSS ecosystem, based on.

![](https://oscimg.oschina.net/oscnet/a4b0417ae1f012c589f13cbe55b0dd5888a.jpg)

### [Software-Defined Telecom: From Open Source to Mainstream, Bringing Complex Technology to the Masses - Anthony Minessale, SignalWire INC](https://opencoresummit2019.sched.com/event/UNKy/software-defined-telecom-from-open-source-to-mainstream-bringing-complex-technology-to-the-masses-anthony-minessale-signalwire-inc)
From the early days of VoIP starting with Asterisk and through the advent of FreeSWITCH and on to WebRTC, Learn how the evolution of Software-Defined Telecom led by SignalWire has brought us to a new era in telecommunications.
> 看样子能 Get 新电信时代不少信息,不知道会给通信这块业务带来怎么样的灵感。

![](https://oscimg.oschina.net/oscnet/944a077fc133cf95b63f23e84bdfdb83712.jpg)

### [Open Source and Open Core – Not a Zero Sum Game - Andi Gutmans, AWS](https://opencoresummit2019.sched.com/event/UNL1/open-source-and-open-core-not-a-zero-sum-game-andi-gutmans-aws)
Gutmans will talk about his early journey in open source, how he’s seen open source evolve over the years, and why the relation between open source and open core is not a zero sum game.

![](https://oscimg.oschina.net/oscnet/1ab786b1baae900c9c1833d2049ac575804.jpg)

### [Dual licensing: its place in an Open Source business, with reflections on a 32-year success story - L Peter Deutsch, Artifex Software](https://opencoresummit2019.sched.com/event/UNL4/dual-licensing-its-place-in-an-open-source-business-with-reflections-on-a-32-year-success-story-l-peter-deutsch-artifex-software)

This talk will cover one of many possible maps of how to structure thinking about Open Source business; how Ghostscript does and doesn't align with that map; what has made Ghostscript successful; and a perspective on dual licensing in general.

![](https://oscimg.oschina.net/oscnet/63e9b4b635a5231e22f8fd021f42556605b.jpg)

### [Disrupting the Enterprise with Open Source - Marco Palladino, Kong](https://opencoresummit2019.sched.com/event/UNL7/disrupting-the-enterprise-with-open-source-marco-palladino-kong)

The Open Source revolution transforms and scales the modern Enterprise by increasing team productivity, and improving business scalability. Specifically when it comes to networking and services, OSS technologies have played an important role in redefining entire applications and architectures, ultimately transforming the organization in a distributed organism. This session explores the open source revolution, and its impacts in the Enterprise, and how it transformed organizations operationally, technologically and culturally.

> 看,又一个开源项目实践分享,不知道有什么新切入点

![](https://oscimg.oschina.net/oscnet/516aeeb52acc28a5419056d45e5c66f2f5b.jpg)

### [The New Business Model - Creating Operational Excellence around Open Source with Cloud - Jason McGee, IBM](https://opencoresummit2019.sched.com/event/UNLA/the-new-business-model-creating-operational-excellence-around-open-source-with-cloud-jason-mcgee-ibm)

Public clouds have risen with an interesting mix of Open Source and Proprietary software and APIs. At IBM, we have built our global public cloud on an open source foundation. In this talk Jason will discuss the value that can be created delivering open source technologies as a service on cloud, including the role Open Source plays in cloud, the power of open source in enabling clients to leverage cloud in a portable way, the incredible combination of open source projects that had to come together to create and operate a full stack cloud platform and the rise of new value and business models for open source that are enabled by as-a-Service cloud delivery.

> IBM 的本次演讲也是围绕着本次大会的热门议题:云计算,包括开源在云中所起的作用、将开源的力量和云移动相结合,最终实现开源云交付服务。

![](https://oscimg.oschina.net/oscnet/caa195d8e54f54cddf09712c5c50d6227c6.jpg)

### [Going Global with Open Core - John Newton, Alfresco Software](https://opencoresummit2019.sched.com/event/UNLD/going-global-with-open-core-john-newton-alfresco-software)

To build a successful open source business, you have to think beyond your project and start thinking globally almost from day one. Taking and contrasting experiences of building both an open source, open core business and a proprietary software business, John presents the advantages and challenges of building a global software business using an open core model. This presentation examines the role of the community, partners, channels and the project in moving beyond a home market.

Some of the issues addressed are: how do you look beyond your home market, how do you hire to grow globally, how does an open core model work in a global market, how do you fund global growth, and how do you compete against giant proprietary incumben.

![](https://oscimg.oschina.net/oscnet/9a0afaa9bafaf0996682332f8006d57d04c.jpg)

### [Making Money with Open Source - Marten Mickos, HackerOne](https://opencoresummit2019.sched.com/event/UkwM/making-money-with-open-source-marten-mickos-hackerone)

As for making money with open source, Marten coined the saying "Some people will spend any amount of time to save money; others will spend money to save time." which is key to figuring out how to make money in open source.

### 图图小感悟

浏览了 Open Core Summit 第一天的演讲摘要,「云计算」和「OSS 技术」是本届峰会的宠儿,由于摘要的内容有限,图图只能和大家小点评了下,欢迎关注 Nebula Graph 的订阅号:Nebula Graph Community 查看会场的演讲 (≧▽≦) 想加入图数据库交流群的请添加微信:NebulaGraphbot

![](https://oscimg.oschina.net/oscnet/acae3c421682228eed4bc46ccaede0efe77.jpg)

> Nebula Graph:一个开源的分布式图数据库。

> GitHub:[https://github.com/vesoft-inc/nebula](https://0x7.me/go2github)

> 知乎:https://www.zhihu.com/org/nebulagraph/posts

> 微博:https://weibo.com/nebulagraph

gnet: 一个轻量级且高性能的 Go 网络库

开源程序panjf2000 发表了文章 • 1 个评论 • 577 次浏览 • 2019-09-18 16:08 • 来自相关话题


gnet












# 博客原文
https://taohuawu.club/go-event-loop-networking-library-gnet

# Github 主页
https://github.com/panjf2000/gnet

欢迎大家围观~~,目前还在持续更新,感兴趣的话可以 star 一下暗中观察哦。

# 简介

`gnet` 是一个基于 Event-Loop 事件驱动的高性能和轻量级网络库。这个库直接使用 [epoll](https://en.wikipedia.org/wiki/Epoll) 和 [kqueue](https://en.wikipedia.org/wiki/Kqueue) 系统调用而非标准 Golang 网络包:[net](https://golang.org/pkg/net/) 来构建网络应用,它的工作原理类似两个开源的网络库:[libuv](https://github.com/libuv/libuv) 和 [libevent](https://github.com/libevent/libevent)。

这个项目存在的价值是提供一个在网络包处理方面能和 [Redis](http://redis.io)、[Haproxy](http://www.haproxy.org) 这两个项目具有相近性能的Go 语言网络服务器框架。

`gnet` 的亮点在于它是一个高性能、轻量级、非阻塞的纯 Go 实现的传输层(TCP/UDP/Unix-Socket)网络库,开发者可以使用 `gnet` 来实现自己的应用层网络协议,从而构建出自己的应用层网络应用:比如在 `gnet` 上实现 HTTP 协议就可以创建出一个 HTTP 服务器 或者 Web 开发框架,实现 Redis 协议就可以创建出自己的 Redis 服务器等等。

**`gnet` 衍生自另一个项目:`evio`,但是性能更好。**

# 功能

- [高性能](#性能测试) 的基于多线程模型的 Event-Loop 事件驱动
- 内置 Round-Robin 轮询负载均衡算法
- 简洁的 APIs
- 基于 Ring-Buffer 的高效内存利用
- 支持多种网络协议:TCP、UDP、Unix Sockets
- 支持两种事件驱动机制:Linux 里的 epoll 以及 FreeBSD 里的 kqueue
- 支持异步写操作
- 允许多个网络监听地址绑定在一个 Event-Loop 上
- 灵活的事件定时器
- SO_REUSEPORT 端口重用

# 核心设计

## 多线程模型

`gnet` 重新设计开发了一个新内置的多线程模型:『主从 Reactor 多线程』,这也是 `netty` 默认的线程模型,下面是这个模型的原理图:


multi_reactor



它的运行流程如下面的时序图:


reactor



现在我正在 `gnet` 里开发一个新的多线程模型:『带线程/go程池的主从 Reactors 多线程』,并且很快就能完成,这个模型的架构图如下所示:


multi_reactor_thread_pool



它的运行流程如下面的时序图:


multi-reactors



## 通信机制

`gnet` 的『主从 Reactors 多线程』模型是基于 Golang 里的 Goroutines的,一个 Reactor 挂载在一个 Goroutine 上,所以在 `gnet` 的这个网络模型里主 Reactor/Goroutine 与从 Reactors/Goroutines 有海量通信的需求,因此 `gnet` 里必须要有一个能在 Goroutines 之间进行高效率的通信的机制,我没有选择 Golang 里的主流方案:基于 Channel 的 CSP 模型,而是选择了性能更好、基于 Ring-Buffer 的 Disruptor 方案。

所以我最终选择了 [go-disruptor](https://github.com/smartystreets-prototypes/go-disruptor):高性能消息分发队列 LMAX Disruptor 的 Golang 实现。

## 自动扩容的 Ring-Buffer

`gnet` 利用 Ring-Buffer 来缓存 TCP 流数据以及管理内存使用。







# 开始使用

## 安装

```sh
$ go get -u github.com/panjf2000/gnet
```

## 使用示例

```go
// ======================== Echo Server implemented with gnet ===========================

package main

import (
"flag"
"fmt"
"log"
"strings"

"github.com/panjf2000/gnet"
"github.com/panjf2000/gnet/ringbuffer"
)

func main() {
var port int
var loops int
var udp bool
var trace bool
var reuseport bool

flag.IntVar(&port, "port", 5000, "server port")
flag.BoolVar(&udp, "udp", false, "listen on udp")
flag.BoolVar(&reuseport, "reuseport", false, "reuseport (SO_REUSEPORT)")
flag.BoolVar(&trace, "trace", false, "print packets to console")
flag.IntVar(&loops, "loops", 0, "num loops")
flag.Parse()

var events gnet.Events
events.NumLoops = loops
events.OnInitComplete = func(srv gnet.Server) (action gnet.Action) {
log.Printf("echo server started on port %d (loops: %d)", port, srv.NumLoops)
if reuseport {
log.Printf("reuseport")
}
return
}
events.React = func(c gnet.Conn, inBuf *ringbuffer.RingBuffer) (out []byte, action gnet.Action) {
top, tail := inBuf.PreReadAll()
out = append(top, tail...)
inBuf.Reset()

if trace {
log.Printf("%s", strings.TrimSpace(string(top)+string(tail)))
}
return
}
scheme := "tcp"
if udp {
scheme = "udp"
}
log.Fatal(gnet.Serve(events, fmt.Sprintf("%s://:%d", scheme, port)))
}

```

## I/O 事件

`gnet` 目前支持的 I/O 事件如下:

- `OnInitComplete` 当 server 初始化完成之后调用。
- `OnOpened` 当连接被打开的时候调用。
- `OnClosed` 当连接被关闭的时候调用。
- `OnDetached` 当主动摘除连接的时候的调用。
- `React` 当 server 端接收到从 client 端发送来的数据的时候调用。(你的核心业务代码一般是写在这个方法里)
- `Tick` 服务器启动的时候会调用一次,之后就以给定的时间间隔定时调用一次,是一个定时器方法。
- `PreWrite` 预先写数据方法,在 server 端写数据回 client 端之前调用。

# 性能测试

## Linux (epoll)

### 系统参数

```powershell
Go Version: go1.12.9 linux/amd64
OS: Ubuntu 18.04
CPU: 8 Virtual CPUs
Memory: 16.0 GiB
```

### Echo Server

![echolinux.png](https://img.hacpai.com/file/2019/09/echolinux-fca6e6e5.png)


### HTTP Server

![httplinux.png](https://img.hacpai.com/file/2019/09/httplinux-663a0318.png)


## FreeBSD (kqueue)

### 系统参数

```powershell
Go Version: go version go1.12.9 darwin/amd64
OS: macOS Mojave 10.14.6
CPU: 4 CPUs
Memory: 8.0 GiB
```

### Echo Server

![echomac.png](https://img.hacpai.com/file/2019/09/echomac-7a29e0d1.png)


### HTTP Server

![httpmac.png](https://img.hacpai.com/file/2019/09/httpmac-cb6d26ea.png)


# 证书

`gnet` 的源码允许用户在遵循 MIT [开源证书](https://github.com/panjf2000/gnet/blob/master/LICENSE) 规则的前提下使用。

# 待做事项

> gnet 还在持续开发的过程中,所以这个仓库的代码和文档会一直持续更新,如果你对 gnet 感兴趣的话,欢迎给这个开源库贡献你的代码~~

图数据库爱好者的聚会在谈论什么?

文章分享NebulaGraph 发表了文章 • 0 个评论 • 681 次浏览 • 2019-09-16 12:23 • 来自相关话题

> [Nebula Graph](https://0x7.me/go2github):一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可 ...查看全部
> [Nebula Graph](https://0x7.me/go2github):一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可用且保障数据安全性。

### 聚会概述

在上周六的聚会中,Nebula Graph Committer 吴敏给爱好者们介绍了整体架构和特性,并随后被各位大佬~~轮番蹂躏~~(划掉)。


本次分享主要介绍了 Nebula Graph 的特性,以及新上线的[《使用 Docker 构建 Nebula Graph》](https://zhuanlan.zhihu.com/p/81316517)功能。

下面是现场的 Topic ( 以下简称:T ) & Discussion ( 以下简称:D ) 速记:

### 讨论话题目录

- 算法和语言
- 图库的 builtin 只搞在线查询可以吗?有必要搞传播算法和最短路径吗?Nebula 怎么实现对图分析算法的支持?
- 为什么要新开发一种查询语言 nGQL?做了哪些优化?
- 对于超大点,有啥优化的办法吗,或者对于构图有什么建议嘛?
- 图库相比其它系统和数据库未来发展趋势,比如相比文档和关系型,它的核心价值是什么?
- 架构和工程
- key 为什么选择用 hash 而不是 range?
- gRPC,bRPC,fbthrift 为什么这么选 rpc?有没有打算自己写一个?
- 图库在设计上趋同化和同质化,架构上还有哪些创新值得尝试?
- 关于生态
- 图的生态怎么打造?和周边其它系统怎么集成融合?

#### 算法和语言

使用 Docker 构建 Nebula Graph 源码

文章分享NebulaGraph 发表了文章 • 0 个评论 • 633 次浏览 • 2019-09-06 10:16 • 来自相关话题

![](https://pic4.zhimg.com/v2-8c5114adb5b955b5a52df78ac2ede317_1200x500.jpg) ### Nebula Graph 介绍 [Nebula ...查看全部
![](https://pic4.zhimg.com/v2-8c5114adb5b955b5a52df78ac2ede317_1200x500.jpg)

### Nebula Graph 介绍

[Nebula Graph](https://0x7.me/go2github) 是开源的高性能分布式图数据库。项目使用 C++ 语言开发,`cmake` 工具构建。其中两个重要的依赖是 Facebook 的 Thrift RPC 框架和 [folly 库](https://github.com/facebook/folly).

由于项目采用了 C++ 14 标准开发,需要使用较新版本的编译器和一些三方库。虽然 Nebula Graph 官方给出了一份[开发者构建指南](https://github.com/vesoft-inc/nebula/blob/master/docs/manual-CN/how-to-build.md),但是在本地构建完整的编译环境依然不是一件轻松的事。

### 开发环境构建

Nebula Graph 依赖较多,且一些第三方库需本地编译安装,为了方便开发者本地编译项目源码, Nebula Graph 官方为大家提供了一个预安装所有依赖的 [docker 镜像]([docker hub](https://hub.docker.com/r/vesoft/nebula-dev))。开发者只需如下的三步即可快速的编译 Nebula Graph 工程,参与 Nebula Graph 的开源贡献:

- 本地安装好 Docker

- 将 [`vesoft/nebula-dev`](https://hub.docker.com/r/vesoft/nebula-dev) 镜像 `pull` 到本地

```shell
$ docker pull vesoft/nebula-dev
```

- 运行 `Docker` 并挂载 Nebula 源码目录到容器的 `/home/nebula` 目录

```shell
$ docker run --rm -ti -v {nebula-root-path}:/home/nebula vesoft/nebula-dev bash
```

> 社区小伙伴@阿东 友情建议:记得把上面的 {nebula-root-path}
替换成你 Nebula Graph 实际 clone 的目录

为了避免每次退出 docker 容器之后,重新键入上述的命令,我们在 [vesoft-inc/nebula-dev-docker](https://github.com/vesoft-inc/nebula-dev-docker.git) 中提供了一个简单的 `build.sh` 脚本,可通过 `./build.sh /path/to/nebula/root/` 进入容器。

- 使用 `cmake` 构建 Nebula 工程

```shell
docker> mkdir _build && cd _build
docker> cmake .. && make -j2
docker> ctest # 执行单元测试
```

### 提醒

Nebula 项目目前主要采用静态依赖的方式编译,加上附加的一些调试信息,所以生产的一些可执行文件会比较占用磁盘空间,建议小伙伴预留 20G 以上的空闲空间给 Nebula 目录 :)

### Docker 加速小 Tips

由于 Docker 镜像文件存储在国外,在 pull 过程中会遇到速度过慢的问题,这里 Nebula Graph 提供一种加速 pull 的方法:通过配置国内地址解决,例如:
- Azure 中国镜像 https://dockerhub.azk8s.cn
- 七牛云 https://reg-mirror.qiniu.com

Linux 小伙伴可在 `/etc/docker/daemon.json` 中加入如下内容(若文件不存在,请新建该文件)

```
{
"registry-mirrors": [
"https://dockerhub.azk8s.cn",
"https://reg-mirror.qiniu.com"
]
}
```
macOS 小伙伴请点击 `Docker Desktop 图标 -> Preferences -> Daemon -> Registry mirrors`。 在列表中添加 `https://dockerhub.azk8s.cn` 和 `https://reg-mirror.qiniu.com` 。修改后,点击 Apply & Restart 按钮, 重启 Docker。

![](https://pic3.zhimg.com/80/v2-6d2dd1b7e5999207ace1b590d31a15ea_hd.jpg)

### Nebula Graph 社区

Nebula Graph 社区是由一群爱好图数据库,共同推进图数据库发展的开发者构成的社区。

本文由 Nebula Graph 社区 Committer 伊兴路贡献,也欢迎阅读本文的你参与到 Nebula Graph 的开发,或向 Nebula Graph 投稿。


### 附录

> Nebula Graph:一个开源的分布式图数据库。

> GitHub:[https://github.com/vesoft-inc/nebula](https://0x7.me/go2github)

> 知乎:https://www.zhihu.com/org/nebulagraph/posts

> 微博:https://weibo.com/nebulagraph

图数据库 Nebula Graph v.1.0.0-beta 已上线

开源程序NebulaGraph 发表了文章 • 0 个评论 • 401 次浏览 • 2019-08-20 20:26 • 来自相关话题

> [Nebula Graph](https://0x7.me/go2github):一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可 ...查看全部
> [Nebula Graph](https://0x7.me/go2github):一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可用且保障数据安全性。

Nebula Graph **v1.0.0-beta** 版本已发布,更新内容如下:

### Storage Engine

- 支持集群部署
- 引入 Raft 一致性协议,支持 Leader 切换
- 存储引擎支持 HBase
- 新增从 HDFS 导入数据功能

### 查询语言 nGQL

- 新增注释功能
- 创建 Space 支持默认属性,新增 `SHOW SPACE` 和 `DROP SPACE` 功能
- 新增获取某 Tag 或 EdgeType 属性功能
- 新增获取某 Tag 或 EdgeType 列表功能
- 新增 `DISTINCT` 过滤重复数据
- 新增 `UNION` , `INTERSECT` 和 `MINUS` 集合运算
- 新增 `FETCH` 获取指定 Vertex 相应 Tag 的属性值
- `WHERE` 和 `YIELD` 支持用户定义变量和管道操作
- `WHERE` 和 `YIELD` 支持算术和逻辑运算
- 新增 `ORDER BY` 对结果集排序
- 支持插入多条点或边
- 支持 HOSTS 的 CRUD 操作

### Tools

- 新增 Java importer - 从 CSV 导入数据
- package_build -  支持 Linux 发行指定版本的软件包
- perf tool - Storage Service 压测工具
- Console 支持关键字自动补全功能

### ChangeLog

- `$$[tag].prop` 变更为 `$$.tag.prop` , `$^[tag].prop` 变更为 `$^.tag.prop` 
- 重构运维脚本 nebula.service

### 附录
最后是 Nebula 的 GitHub 地址,欢迎大家试用,有什么问题可以向我们提 issue。GitHub 地址:[github.com/vesoft-inc/nebula](https://0x7.me/go2github)  ;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot

图数据库综述与 Nebula 在图数据库设计的实践

文章分享NebulaGraph 发表了文章 • 0 个评论 • 708 次浏览 • 2019-08-12 14:16 • 来自相关话题

> [Nebula Graph](https://0x7.me/go2github):一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可 ...查看全部
> [Nebula Graph](https://0x7.me/go2github):一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可用且保障数据安全性。

第三期 nMeetup( nMeetup 全称:Nebula Graph Meetup,为由开源的分布式图数据库 Nebula Graph 发起的面向图数据库爱好者的线下沙龙) 活动于 2019 年 8 月 3 日在上海陆家嘴的汇丰银行大楼举办,我司 CEO -- Sherman 在活动中发表《 Nebula Graph Internals 》主题演讲 。本篇文章是根据此次演讲所整理出的技术干货,全文阅读需要 30 分钟,我们一起打开图数据库的知识大门吧~

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316395313-b80741dd-61c4-4adb-904e-2bd8b53be139.png)

大家好,非常感谢大家今天能够来我们这个线下沙龙,天气很热,刚又下了暴雨,说明大家对图数据库的热情要比夏天温度要高。今天我们准备了几个 topic,一个就是介绍一下我们产品——Nebula 的一些设计思路,今天不讲介绍性东西,主要讲 Nebula 内部的思考——为什么会去做 Nebula,怎么样去做,以及为什么会采取这样的设计思路。

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565337816231-6ee9c2fb-9455-4ff4-b8ab-0495ebfe8686.png)

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316413003-a5405fc3-f736-48b2-99d3-572e6e97e9a3.png#)

这个就是 Nebula。先从 overview 介绍图数据库到底是个什么东西,然后讲我们对图数据库的一些思考。最后具体介绍两个模块, Nebula 的 Query Service 和 Storage Service。后面两部分会稍微偏技术一些,在这个过程当中如果大家遇到什么问题,欢迎随时提出。

## 图数据库是什么
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316422046-0900a5e7-ce5b-4e6c-a227-5d3092243485.png)

### 图领域的 OLAP & OLTP 场景
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316431355-72267be6-228f-4182-ba3f-66f004bf6693.png)

对于图计算或者图数据库本身我们是这么理解的,它跟传统数据库很类似,也分为 OLAP 和 OLTP 两个方向。

上图中下面这根轴表示数据对查询时效性的要求,OLAP 更偏向于做离线分析,OLTP 更偏向于线上处理。我们认为图领域也可以这么划分。

首先在 OLAP 这个领域,我们称为图计算框架,这些图计算框架的主要目的,是做分析。基于图的结构的分析——这里图代指一个关系网络。整体上这个分析比较类似于传统数据库的 OLAP。但一个主要特点就是基于图结构的迭代算法,这个在传统数据库上是没有的。最典型的算法是谷歌 PageRank,通过一些不断的迭代计算,来算网页的相关度,还有非常常见的 LPA 算法,使用的也非常多。

如果我们继续沿着这个线向右边延伸,可以看到一个叫做 Graph Stream 的领域,这个领域相当于图的基础计算跟流式计算相结合的产物。大家知道关系网络并不是单单一个静态结构,而是会在业务中不断地发生变化:可能是图的结构,或者图上的属性发生变化,当图结构和属性发生变化时,我们希望去做一些计算,这里的计算可能是一种触发的计算或判断——例如在变化过程当中是不是动态地在图上形成一个闭环,或者动态地判断图上是否形成一个隔离子图等等。Trigger(触发)的话,一般是通过事件来驱动这类计算。对时效性的响应当然是越高越好,但往往响应时间一般是在秒级左右。

那么再往轴上右边这个方向看,是线上的在线响应的系统。大家知道这类系统的特点是对延时要求非常高。可以想象如果在线上做交易的时候,在这个交易瞬间,需要到图上去拿一些信息,当然不希望要花费秒级。一般响应时间都是在十几、二十毫秒这样一个范围。可以看到最右边的场景和要求跟左边的是完全不一样的,是一种典型的 OLTP 场景。在这种场景里面,通常是对子图的计算或者遍历——这个和左边对全图做计算是完全不一样的:比如说从几个点出发,得到周边3、4度的邻居构成一个子图,再基于这个子图进行计算,根据计算的结果再继续做一些图遍历。所以我们把这种场景称为图数据库。我们现在主要研发内容主要面向OLTP这类场景。所以说今天的一些想法和讲的内容,都是跟这块相关。

### 图数据库及其他数据库的关注度增长趋势

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316446183-1bd35a93-677c-4661-9e9d-9e44cbe89676.png)

这张图是从 DB-Engines.com 上截下来的,反应了从 2013 年到 2019 年 7 月,所有类型数据库的趋势。这个趋势是怎么计算出来的?DB-Engines 通过到各大网站上去爬取内容,查看所有用户,包括开发人员和业务人员的情况下统计某类数据库被提及的次数将其转化为分数,分数越高,表示这种类型的数据库的关注度越高。所以说这是一个关注度的趋势。

最底下这条红线是关系型数据库,在关系型数据库之上有各类数据库,比如 Key-Value型、文档型、RDF 等。最上面的绿线就是图数据库。

可以看到在过去六年多的时间里,图数据库的整个趋势,或者说它的影响力大概增长了十倍。

### 图数据存储计算什么数据
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316455530-b367bb83-93c2-4922-ba51-97dbdd5ce7d9.png)

今天我们谈数据库肯定离不开数据,因为数据库只是一个载体,一个存储和计算的载体,它里面的数据到底是什么呢?就是我们平时说的图。这里列出了几个目前为止比较常见的多对多的关系数据。

#### 图数据的常见多对多关系数据库场景

第一个 Social Network(社交网路),比如说微信或者 Facebook 好友关系等等。这个网络有几十亿个用户,几千亿到几万亿的连接关系。第二个 Business Relation,商业的关系,常见的有两种网络:

- 金融与资金关系网络:一种比如说在支付网络里面,账户和账户之间的支付关系或者转账关系,这个是比较典型的金融与资金关系网络;
- 公司关系:在 business 里面,比如说公司控股关系,法人关系等等,它也是一个非常庞大的网络。基于工商总局数据,有许多公司耕耘在这一领域。这个网络的节点规模也有亿到十亿的级别,大概几百亿条边,如果算上交易转账数据那就非常庞大了。

第三个是知识图谱,也是最近比较热的一个领域。在各个垂直领域会有不同的知识点,且知识点之间有相关性。部分垂直领域知识的网络至少有几百亿条关系,比如银行、公安还有医学领域。

最后就是这几年热门的 IoT(Internet of Things)领域,随着近年智能设备的增长,预计以后 IoT 设备数量会远超过人口数量,现在我们每个人身边佩带的智能设备已不止一个,比如说智能手机、智能手表,它们之间组成一个庞大的关系网络,虽然具体应用有待后续开发,但这个领域在未来会有很大的应用空间。

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316464166-e5aeeea1-77df-46ce-9a65-893f1d1c8974.png)

#### 图数据库的应用场景

刚才提到的是常见的关系网络,这里是我们思考的一些应用场景。

第一个应用场景是基于社交关系网络的社交推荐,比如:拼多多的商品推荐,抖音的视频推荐,头条的内容推荐,都可以基于已有的好友关系来推荐。

第二个就是风控领域,风控其实是银行保险业的核心话题。传统的风控是基于规则——基于规则的风控手段,相对已经比较成熟了,一般是拿直接的交易对手来做规则判断。但现在风控有个新趋势,就是通过关联关系做拓展,比如交易对手等相关的周边账号,通过这些关系来判断这笔交易或者转账的风险。从规则向基于关联关系的风控演进,这个趋势比较明显。

关于知识图谱这一块,和 Google 比较有关,谷歌在 2003,2004 年时候,其实已经在慢慢把它的 search engine,从反向索引转向转到了知识图谱。如果只有倒排表,比如说要查“特朗普”今年几岁,这个是很难做到的,因为已有的信息是他的生日是哪年。

这几年机器学习和 AI 领域发展非常快,大家知道就机器学习或者模型训练范畴来说,平时用大量数据去训练模型,其实归根结底是对大量数据汇总或者说统计性的结果。最近一两年,大家发现光有统计性结果不够,数据和数据之间的关系也应体现在模型里,所以开始将基于图的数据关系加入到模型训练,这个就是学术界非常流行的 Graph Embedding,把图的结构引入到模型训练里面。

在健康和医疗领域,患者的过往病史、服药史、医生的处方还存在纸质文档的情况,一些医疗类公司通过语音和图像将文档数字化,再用 NLP 把关键信息提取出来。根据关键信息,比如:血压、用药等等构造一棵大的决策树或者医疗知识图谱。这块也是比较新的应用。

区块链的应用其实比较容易理解,区块链本身虽然说是链,但有很多分支结构,当分支交织后也就构成一个网络。举个简单例子,A 某想通过比特币洗钱,常用方法是通过多个账号,几次转账后,资金通过数字货币形成一个闭环,而这个方法是可以通过图进行洗钱防范。

最后一块是公共安全领域,比如,某些犯罪是团伙作案,那么追踪团伙中某个人的行为轨迹,比如:交通工具、酒店等等就可标识出整个团伙的特征。某个摄像头和某个嫌疑人在某个时间构建起来关联关系,下一个时刻,另外一个摄像头和另外一个嫌疑人也建立了关联。这个图不是静态的,它是时序的。
这些就是一些已经看到的图的应用领域。

### 图数据库面临的挑战

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316479943-3417553e-2e80-4551-95b6-c598c892eca9.png)

回到图数据库,做图数据库到底有哪些挑战。和所有的 OLTP 系统一样:

第一个挑战就是**低延时**。我们不希望一次查询,要几秒钟甚至几分钟才能产生结果。比如说风控场景,在线转账的时候,我要判断这笔交易是否有风险,可能整个时间只有一百毫秒,留给风控判断的时间只有几十毫秒。不可能转账完才发现对方账户已经被标黑了,或者这笔交易其实是在套null现。

第二个挑战是**高吞吐**,现在热门的 APP,比如抖音或者头条,日常访问的并发量是非常高的,峰值可能几十万 QPS,DB 要能抗的住。

第三个挑战是**数据量激增**,数据量的增加速度快于硬件特别是硬盘的增长速度,这个给 DB 带来了很大的挑战。大家可能用过一些单机版的图数据库,刚开始用觉得不错,能满足需求。但一两年后,发现数据量增加太快,单机版已经完全满足不了需求,这时总不能把业务流控吧。

我们遇到过一个图 case,有超过一千亿个节点,一万亿条边,点和边上都还有属性,整个图的数据量超过上百T。可以预计下,未来几年数据量的增长速度会远远快于摩尔定理的速度,所以单机版数据库肯定搞不定业务需求,这对图数据库开发是一个很大的挑战。

第四个挑战是**分析的复杂性**,当然这里分析指的是 OLTP 层面的。因为图数据库还比较新,大家刚开始使用的时候会比较简单,例如只做一些 K度拓展。但是随着使用者的理解越来越深,就会提出更多越来越复杂的需求。例如在图遍历过程中过滤、统计、排序、循环等等,再根据这些计算结果继续图遍历。所以说业务需求越来越复杂。这就要求图数据库提供的功能越来越多。

最后一个挑战是关于**数据一致性**——当然还有很多其他挑战,这里没有全部罗列。前几年大家对于图数据库的使用方法更像使用二级索引,把较大的数据放在另外的存储组件,比如 HBase 将关联关系放在图数据库里,将图数据库只作为图结构索引来加速。但像刚才说的,业务越来越复杂,对响应时间要求越来越高,原先的架构除了不方便,性能上也有很大挑战。比如,需要对属性做过滤时,要从 HBase 读取出太多数据,各种序列化、过滤操作都很慢。这样就产生了新需求——将这些数据直接存储在图数据库里,自然 ACID 的需求也都有了。

### 图数据库模型:原生图数据库 vs 多模数据库
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316491849-b28c48e1-d71b-4c99-850b-0f3c80481825.png)

说完技术挑战,还有个概念我想特别澄清下。大家如果网上搜图数据库,可能有 20 个自称图数据库的产品。我认为这些产品可以分成两类,一种就是**原生**的,还有一类是**多模**的。

对于图原生的产品,在设计时考虑了图数据的特性,存储、计算引擎都是基于图的特点做了特别设计和优化。

而对于多模的产品,就有很多,比如说 ArangoDB 或者 Orientdb,还有一些云厂商的服务。它们的底层是一个表或文档型数据库,在上层增加图的服务。对于这类多模数据库,图服务层所做的操作,比如:遍历、写入,最终将被映射到下面的存储层,成为一系列表和文档的操作。所以它最大的问题是整个系统并不是为了图这种多对多的结构特点设计,一旦数据量或者并发量增大之后,问题就比较明显。我们最近碰到一个比较典型的 case,客户使用多模 DB,在数据量很小时还比较方便,但当数据量大到一定程度,做二跳三跳查询时 touch 的数据非常多,而多模 DB 底层是关系型数据库,所有关系最终要映射到关系型数据库的 `join`  操作,做三四层的 `join` ,性能会非常的差。

### 图数据库——Nebula Graph:一个开源的分布式图数据库
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316501138-7ff22a15-8912-4877-9e9d-fb8102e8039d.png)

上面是我们对行业的一些思考。这里是我们在做的图数据库,它是一个开源的分布式的项目——[Nebula Graph](https://github.com/vesoft-inc/nebula)。

#### 存储设计
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316509130-47d07bbb-1d33-4a1a-90f6-ffa600d1b0b2.png)

这里我想说下我们在设计 Nebula 时候的一些思考,为什么会这样设计?

刚刚说到过技术挑战中数据量迅速膨胀,业务逻辑越发复杂,像这样的开发挑战,Nebula 是如何解决的。

Nebula 在设计存储时,采用 share-nothing 的分布式架构,本质上存储节点间没有数据共享,也就是整个分布式结构无中心节点。这样的好处在于,第一,容易做水平拓展;第二,即使部分机器 Crash,通过数据强一致性—— Raft 协议能保证整个系统的可用性,不会丢失数据。

因为业务会越发复杂,所以 Nebula 支持多种存储引擎。有的业务数据量不大但对查询的实时性要求高,Nebula 支持纯内存式的存储引擎;数据量大且查询并发量也大的情况下,Nebula 支持使用内存加 SSD 模式。当然 Nebula 也支持第三方存储引擎,比如,HBase,考虑到这样使用存在的主要问题是性能不佳,我们建议用在一些对性能要求不是很高的场景。

第三个设计就是把存储和计算这两层分开了——也就是“存储计算分离”。这样的设计有几个明显的好处。所有数据库在计算层通常都是无状态的,CPU intensive,当 CPU 的计算力不够的时候,容易弹性扩容、缩容,而对于存储层而言,涉及到数据的搬迁情况要复杂些。所以当存储计算分离后,计算层和存储层可以根据各自的情况弹性调整。

至于数据强一致这个挑战,有主要分两个方面,一个是关于数据的强一致,就是多数派协议——Nebula 现在使用的 Raft 协议,通过多副本的方式来实现强一致。另外个是分布式的事物务 Transaction,它来保证要向多台机器写入一批相互依赖数据的正确性,这个和 NewSQL 里面的概念是非常类似的。

#### 计算设计
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316533389-f15a9475-b9ac-4a59-bbc3-55c52d2366f1.png)

刚刚是我们对存储引擎的一些思考,这里是我们对计算引擎的思考。

前面提到的一个技术挑战是低延时、高并发,Nebula 整个的核心代码都是 C++ 写的,这样保证了执行效率。其次,做了很多并行和异步执行的优化。第三个是计算下推。在分布式系统里面,硬件上网络对整体性能的影响最大,所以数据搬迁是一个很低效的动作。有些开源图数据库产品,比如 JanusGraph,它的存储层在 HBase,上面有个单独的计算层,当计算层需要数据的时候,会到 HBase 里面拉回大量的数据,再做过滤和计算。举个例子,1 万条数据里面最终过滤出 100 条,那相当于 99% 的网络传输都浪费了。所以 Nebula 的设计方案是移动计算,而不是数据,计算下推到存储层,像前面这个例子,直接在存储层做完过滤再回传计算层,这样可以有 100 倍的加速。

第二,如果大家接触过图数据库领域的一些产品,会发现图数据库这领域,相比关系型数据库有个很大的问题——没有通用的标准。关系型领域的标准在差不多 30 年前已制定,但图数据库这个领域各家产品的语言相差很大。那么针对这个问题 Nebula 是怎么解决?第一尽量贴近 SQL,哪怕你没有学过 Nebula 语言,你也能猜出语句的作用。因此 Nebula 的查询语言和 SQL 很像,为描述性语言,而不是命令式语言。第二个是过去几年我们做图数据库领域的经验积累,就是 No-embedding(无嵌套)。SQL 是允许 embedding 的,但嵌套有个问题——当查询语句过长时,语句难读,因为 SQL 语句需从内向外读,语序正好跟人的理解相反,因为人比较习惯从前往后来理解。所以Nebula 把嵌套语句写成从前往后的方式,作为替代,提供 Shell 管道这样的方式,即前面一条命令的输出作为后一条命令的输入。这样写出来的语句比较容易理解,写了一个上百行的 query 你就会发现从前往后读比从中间开始读要易于理解。
第三,和传统数据库相比,图的计算不光是 CRUD,更重要是基于图结构的算法,加上新的图算法不停地涌现,Nebula 怎么 keep up?

- 将部分主要算法 build in 到查询引擎里;
- 通过支持 UDF(user-defined function,用户定义函数),用户可把业务相关逻辑写成程序或者函数,避免写重复 query;
- 查询语言的编程能力:Nebula 的查询语言 nGQL 是 Programmable。

#### 架构设计
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316561369-18ed8110-1928-42d5-aea3-0b41aa1ee4ff.png)

Overview 这一章节的最后内容是 Nebula 的架构。上图虚线把存储和计算一分为二,上面是整个计算层,每一台机器或者虚拟机是一个节点,它是无状态的,相互之间没有任何通讯,所以计算层要做扩缩容非常容易。
下面是存储层,有数据存储所以是有状态的。存储层还可以细分,第一层是 Storage Service,从计算层传来的请求带有图语义信息,比如:邻居点、取 property,Storage Service 的作用是把图语义变成了 key-value 语义交给下层的 KV-store,在 Storage Service 和 KV-store 之间是一层分布式 Raft 协议。

图的右边是 Meta Service,这个模块有点类似 HDFS 的 NameNode,主要提供两个功能,一个是各种元信息,比如 schema,还有一个是指挥存储扩容。大家都知道存储扩容是个非常复杂的过程,在不影响在线业务地情况下一步步增加机器,数据也要一点点搬迁,这个时候就需要有个中心指挥。另外 Meta Service 本身也是通过 Raft 来保证高可用的。

## 图数据库 Nebula 的查询引擎设计
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316581459-ce28c72c-acc0-4f36-9b2c-91613a2ef28d.png)

上面就是 Nebula 的总体介绍,下面这个部分介绍查询引擎的设计细节。

### 查询语言——nGQL
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316588889-b39614ec-2d99-48a4-b334-1533a5e1692b.png)

先来介绍下 Nebula 的查询语言 nGQL。nGQL 的子查询有三种组合方式:管道、分号和变量。
nGQL 支持实时的增删改、图遍历、属性遍历,也支持对属性先做 index 再遍历。此外,你还可以对图上的路径写个正则表达式,查找所有满足这个条件的图路径。

### 查询引擎架构
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316603924-6d989dd0-2343-4a88-b1c6-834da892400b.png)

再来介绍下查询引擎的架构,从查询引擎的架构图上来看,和数据库类似:从客户端发起一个请求(statement),然后 Parser 做词法分析,之后把分析结果传给执行计划(Execution Planner),执行计划会基于这个语句的特点,交给优化器(Optimizer)进行优化,最后将结果交给执行引擎(Execution Engine)去执行。执行引擎会通过 MetaService 获取点和边的 schema,通过存储引擎层获取点和边的数据。

### 执行计划案例
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316612138-e1cca82e-3d07-47d9-906d-e3d7ac17cc08.png)

这里有个小例子展示了执行计划,下方就是一条语句。首先 use myspace,这里的 space 和数据库里的 database 是一个概念,每个 space 是一个物理隔离的空间,可用来区分敏感数据和非敏感数据,或者说做多租户的支持。分号后面是下一语句—— `INSERT VERTEX` 插入节点, `GO` 是网络拓展, `|` 为 Nebula 的管道用法,这条语句的意思是将第一条 Go 的结果传给第二条 Go,然后再传给第三条,即往外走三步遍历,最后把整个结果做求和运算。这是一种常见的写法,Nebula 还支持其他写法。

这样语句的执行计划就会变成上图右边的语法执行树。这里每个节点,都叫做Executor(语法执行者),语句中的分号对应执行 `SequentialExecutor` ,Go 对应执行 `GoExecutor` ,"|"(管道)对应执行 `PipeExecutor` 。

### 执行优化
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316621670-da45b911-47d0-4749-a37f-b5aefbddffb2.png)

这一页介绍执行优化。在顺序执行过程中优化器会判断当前语句是否存在相互依赖关系,在没有相互依赖时,执行引擎可并行执行从而加速整个执行过程,降低执行延时。流水线优化,跟处理器 CPU 的流水线优化类似。上面“GO … | GO … | GO … ”例子中,表面上第一个 GO 执行完毕后再把结果发给第二个 GO 执行,但实际执行时,第一个 GO 部分结果出来之后,就可以先传给下一个GO,不需要等全部结果拿到之后再传给下一步,这对提升时延效果明显。当然不是所有情况都能这样优化,里面有很多工作要做。前面已提过计算下沉的优化,即过滤的操作尽量放在存储层,节省通过网络传输的数据量。JIT 优化在数据库里已经比较流行,把一条 query 变成代码直接去执行,这样执行效率会更高。

## 图数据库 Nebula 的存储引擎设计
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316631002-29a58273-ce4e-4acd-bec7-0eab273d2c6f.png)

刚才介绍了查询引擎,下面介绍存储引擎。

### 存储架构
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316637994-b7fc8669-f01d-4806-9be8-82d9bdf21296.png)

这张图其实是前面整体架构图的下面部分。纵向可理解为一台机器,每台机器最上面是 Storage service,绿色的桶是数据存储,数据被切分成很多个分片 partition,3 台机器上同 id 的 partition 组成一个 group,每个 group 内用 Raft 协议实现多副本一致性。Partition 的数据最后落在 Store Engine 上,Nebula 默认 Store Engine 是 RocksDB。这里再提一下,partition 是个逻辑概念,partition 数量多少不会额外增加内存,所以一般把 partition 数量设置成远大于机器的数量。

### Schema
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316646278-5ded0550-c211-4fe5-b86e-9a2dc40b6ef3.png)

这一页是 schema,讲的是怎么把图数据变成 KV 存储。这里面的第一个概念是标签(Tag),“标签”表示的是点(Vertex)的类型,一个点可以有多种“标签”或者说“类型”。另一个概念是边类型(Edge Type),一条边需要用起点 ID,终点 ID,边类型和 Ranking 来唯一标识。前面几个字段比较好理解,Ranking 这个字段是留给客户表示额外信息,比如:转账时间。这里补充下说明下 Nebula 顶点 Vertex、标签 Tag、边 Edge、边类型 Edge Type的关系。

Vertex 是一个顶点,用一个 64 位的 id 来标识,一个 Vertex 可以被打上多个 Tag(标签),每个 Tag 定义了一组属性,举个例子,我们可以有 Person 和 Developer 这两个 Tag,Person 这个 Tag 里定义了姓名、电话、住址等等信息,Developer 这个 Tag 里可能定义了熟悉的编程语言、工作年限、GitHub 账号等等信息。一个 Vertex 可以被打上 Person 这个 Tag,这就表示这个 Vertex 代表了一个 Person,同时也包含了 Person 里的属性。另一个 Vertex 可能被同时打上了 Person 和 Developer 这两个 Tag,那就表示这个 Vertex 不仅是一个 Person,还是一个 Developer。

Vertex 和 Vertex 之间可以用 Edge 相连,每一条 Edge 都会有类型,比如:好友关系。每个 Edge Type 可定义一组属性。Edge 一般用来表示一种关系,或者一个动作。比如,Peraon A 给 Person B 转了一笔钱,那 A 和 B 之间就会有一条 Transfer 类型的边,Transfer 这个边类型(Edge Type)可以定义一组属性,比如转账金额,转账时间等等。任何两个 Vertex 之间可以有多种类型的边,也可以有多条同种类型的边,比如:转账,两个 Person 间可存在多笔转账,那每笔转账就是一条边。
上面例子中,点和边都带有属性,即多组。Nebula是一个强 schema 系统,属性的每个字段名和对应的类型需要在构图时先定义,和数据库中的 alter table 类似 Nebula 也支持你在后续操作中进行 Schema 更改。Schema 中还有常见的 TTL(Time To Live),指定特定数据的生命周期,数据时效到后这条数据会被自动清理。

### 数据分片和 Key 的设计
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316665777-8c9e2593-9758-4d9d-a744-7e6617aa5d74.png)

刚才有提到过分片(Partition)和键(Key)的设计,这里再详细解释一下。

数据分片 Partition 或者有些系统叫 Shard,它的 ID 是怎么得到?非常简单,根据点的 ID 做 Hash,然后取模 partition 数量,就得到了 PartitionID。

Vertex 的 Key 是由 PartID + VID + TagID 三元组构成的,Value 里面存放的是属性(Property),这些属性是一系列 KV 对的序列化。

Edge 的 Key 是由 PartID + SrcID + EdgeType + Ranking + DstID 五元组构成,但边和点不同:一个点在 Nebula 里只存储一个 KV,但在 Nebula 中一条边会存两个 KV,一个 Out-edge Key和一个 In-edge Key,Out-edge 为图论中的出边,In-edge 为图论中的入边,虽然两个 Key 都表示同一条逻辑边,但存储两个 KV 的好处在于遍历时,方便做出度和入度的遍历。

举个例子:要查询过去 24 小时给我转过钱的人,即查找所有指向我的账号,遍历的时候从“我”这个节点开始,沿着边反向走可以看到 Key 的设计是入边 In-edge 的 DstID 信息在前面。所以做 Key 查询时,入边和终点,也就是“我”和“指向我的边”是存储在一个分片 Partition 上的,这样就不涉及跨网络开销,最多一次硬盘读就可以取到对应数据。

### 多副本和高可用
![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316704699-fb374731-de27-4666-9a30-67d46d1d002b.png)

最后再谈下数据多副本和 Failover 的问题。前面已经提到多副本间是基于 Raft 协议的数据强一致。Nebula 在 Raft 做了些改进,比如,每组 partition 的 leader 都是打散的,这样才可以在多台机器并发操作。此外还增加了 Raft learner 的角色,这个角色不会参与 Raft 协议的投票,只负责从 leader 读数据。这个主要应用于一个数据中心向另外一个数据中心做异步复制场景,也可用于复制到另外第三方存储引擎,比如:HBase。

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316716467-bb028c7d-b121-4520-b561-996499609708.png)

### 容错机制
Nebula 对于容错或者说高可用的保证,主要依赖于 Raft 协议。这样单机 Crash 对服务是没有影响的,因为用了 3 副本。那要是 Meta Server 挂了,也不会像 HDFS 的 NameNode 挂了影响那么大,这时只是不能新建 schema,但是数据读写没有影响,这样做 meta 的迁移或者扩容也比较方便。

![image.png](https://cdn.nlark.com/yuque/0/2019/png/208152/1565316723938-af9627d5-caba-4b43-885b-8fb9b73936e1.png)

最后是 Nebula 的 GitHub 地址,欢迎大家试用,有什么问题可以向我们提 issue。GitHub 地址:[github.com/vesoft-inc/nebula](https://0x7.me/go2github)

> Nebula Graph:一个开源的分布式图数据库。

> GitHub:[github.com/vesoft-inc/nebula](https://0x7.me/go2github)

> 知乎:https://www.zhihu.com/org/nebulagraph/posts

> 微博:https://weibo.com/nebulagraph

go + koa = ? 一个新的web框架 goa 诞生

Go开源项目nicholascao 发表了文章 • 1 个评论 • 491 次浏览 • 2019-08-08 15:52 • 来自相关话题

## koajs 相信绝大部分使用nodejs的开发者都知道[koa](https://koa.bootcss.com/),甚至每天都在跟koa打交道。 ## goa 最近因工作需要 ...查看全部
## koajs

相信绝大部分使用nodejs的开发者都知道[koa](https://koa.bootcss.com/),甚至每天都在跟koa打交道。

## goa

最近因工作需要从nodejs转到go,因此开发了一个koa for golang的web框架--goa。
几乎一样的语法,一样基于中间件。

github地址:[goa](https://github.com/goa-go/goa)

demo:

``` golang
package main

import (
"fmt"
"time"

"github.com/goa-go/goa"
"github.com/goa-go/goa/router"
)

func logger(c *goa.Context, next func()) {
start := time.Now()

fmt.Printf("[%s] <-- %s %s\n", start.Format("2006-6-2 15:04:05"), c.Method, c.URL)
next()
fmt.Printf("[%s] --> %s %s %d%s\n", time.Now().Format("2006-6-2 15:04:05"), c.Method, c.URL, time.Since(start).Nanoseconds()/1e6, "ms")
}

func json(c *goa.Context) {
c.JSON(goa.M{
"string": "string",
"int": 1,
"json": goa.M{
"key": "value",
},
})
}

func main() {
app := goa.New()
router := router.New()

router.GET("/", func(c *goa.Context) {
c.String("hello world")
})
router.GET("/json", json)

app.Use(logger)
app.Use(router.Routes())
app.Listen(":3000")
}
```
如果觉得这个项目不错的话,请给个star给予作者鼓励,
另外欢迎fork和加入开发团队共建。

再次贴上地址https://github.com/goa-go/goa

【Zinx第一章-引言】Golang轻量级并发服务器框架

文章分享Aceld 发表了文章 • 0 个评论 • 563 次浏览 • 2019-04-29 11:44 • 来自相关话题

【Zinx教程目录】 [Zinx源代码](https://github.com/aceld/zinx) https://github.com/aceld/zinx [完整教程电子版(在线高清)- ...查看全部
【Zinx教程目录】
[Zinx源代码](https://github.com/aceld/zinx)

https://github.com/aceld/zinx

[完整教程电子版(在线高清)-下载](https://legacy.gitbook.com/book/aceld/zinx/details)

[Zinx框架视频教程(框架篇)(完整版下载)链接在下面正文](https://www.jianshu.com/p/59418bfb1ff2)

[Zinx框架视频教程(应用篇)(完整版下载)链接在下面正文](https://www.jianshu.com/p/f9d8b1ad288e)

[Zinx开发API文档](https://www.jianshu.com/p/90fe2c7ffbb0)

[Zinx第一章-引言](https://www.jianshu.com/p/23d07c0a28e5)

[Zinx第二章-初识Zinx框架](https://www.jianshu.com/p/e112e850440f)

[Zinx第三章-基础路由模块](https://www.jianshu.com/p/3fbd69ec3dcc)

[Zinx第四章-全局配置](https://www.jianshu.com/p/f2758c1b8f28)

[Zinx第五章-消息封装](https://www.jianshu.com/p/95b57c7e8c5c)

[Zinx第六章-多路由模式](https://www.jianshu.com/p/7e3462f1d942)

[Zinx第七章-读写分离模型](https://www.jianshu.com/p/d69e97a5e45e)

[Zinx第八章-消息队列及多任务](https://www.jianshu.com/p/febcd455627b)

[Zinx第九章-链接管理](https://www.jianshu.com/p/f52c9598fce6)

[Zinx第十章-连接属性设置](https://www.jianshu.com/p/7e7ec64af1e2)

---
【Zinx应用案例-MMO多人在线游戏】

[(1)案例介绍](https://www.jianshu.com/p/5084c8688d93)

[(2)AOI兴趣点算法](https://www.jianshu.com/p/e5b5db9fa6fe)

[(3)数据传输协议protocol buffer](https://www.jianshu.com/p/aa27113b3daf)

[(4)Proto3协议定义](https://www.jianshu.com/p/dfc9386f70a2)

[(5)构建项目及用户上线](https://www.jianshu.com/p/b5082a493c12)

[(6)世界聊天](https://www.jianshu.com/p/12c95e0f1669)

[(7)上线位置信息同步](https://www.jianshu.com/p/dc901a1dbff1)

[(8)移动位置与AOI广播](https://www.jianshu.com/p/8c8fafdace14)

[(9)玩家下线](https://www.jianshu.com/p/43eec70cfa6d)
---

### 1、写在前面

​ 我们为什么要做Zinx,Golang目前在服务器的应用框架很多,但是应用在游戏领域或者其他长链接的领域的轻量级企业框架甚少。

​ 设计Zinx的目的是我们可以通过Zinx框架来了解基于Golang编写一个TCP服务器的整体轮廓,让更多的Golang爱好者能深入浅出的去学习和认识这个领域。

​ Zinx框架的项目制作采用编码和学习教程同步进行,将开发的全部递进和迭代思维带入教程中,而不是一下子给大家一个非常完整的框架去学习,让很多人一头雾水,不知道该如何学起。

​ 教程会一个版本一个版本迭代,每个版本的添加功能都是微小的,让一个服务框架小白,循序渐进的曲线方式了解服务器框架的领域。

​ 当然,最后希望Zinx会有更多的人加入,给我们提出宝贵的意见,让Zinx成为真正的解决企业的服务器框架!在此感谢您的关注!


## 二、初探Zinx架构

![1-Zinx框架.png](https://upload-images.jianshu.io/upload_images/11093205-c75ff682233b2536.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![zinx-start.gif](https://upload-images.jianshu.io/upload_images/11093205-490b49098ac63af6.gif?imageMogr2/auto-orient/strip)


## 三、Zinx开发API文档

### 快速开始

#### server
基于Zinx框架开发的服务器应用,主函数步骤比较精简,最多主需要3步即可。
1. 创建server句柄
2. 配置自定义路由及业务
3. 启动服务

```go
func main() {
//1 创建一个server句柄
s := znet.NewServer()

//2 配置路由
s.AddRouter(0, &PingRouter{})

//3 开启服务
s.Serve()
}
```

其中自定义路由及业务配置方式如下:
```go
import (
"fmt"
"zinx/ziface"
"zinx/znet"
)

//ping test 自定义路由
type PingRouter struct {
znet.BaseRouter
}

//Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
//先读取客户端的数据
fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))

//再回写ping...ping...ping
err := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping"))
if err != nil {
fmt.Println(err)
}
}
```

#### client
Zinx的消息处理采用,`[MsgLength]|[MsgID]|[Data]`的封包格式
```go
package main

import (
"fmt"
"io"
"net"
"time"
"zinx/znet"
)

/*
模拟客户端
*/
func main() {

fmt.Println("Client Test ... start")
//3秒之后发起测试请求,给服务端开启服务的机会
time.Sleep(3 * time.Second)

conn,err := net.Dial("tcp", "127.0.0.1:7777")
if err != nil {
fmt.Println("client start err, exit!")
return
}

for n := 3; n >= 0; n-- {
//发封包message消息
dp := znet.NewDataPack()
msg, _ := dp.Pack(znet.NewMsgPackage(0,[]byte("Zinx Client Test Message")))
_, err := conn.Write(msg)
if err !=nil {
fmt.Println("write error err ", err)
return
}

//先读出流中的head部分
headData := make([]byte, dp.GetHeadLen())
_, err = io.ReadFull(conn, headData) //ReadFull 会把msg填充满为止
if err != nil {
fmt.Println("read head error")
break
}
//将headData字节流 拆包到msg中
msgHead, err := dp.Unpack(headData)
if err != nil {
fmt.Println("server unpack err:", err)
return
}

if msgHead.GetDataLen() > 0 {
//msg 是有data数据的,需要再次读取data数据
msg := msgHead.(*znet.Message)
msg.Data = make([]byte, msg.GetDataLen())

//根据dataLen从io中读取字节流
_, err := io.ReadFull(conn, msg.Data)
if err != nil {
fmt.Println("server unpack data err:", err)
return
}

fmt.Println("==> Recv Msg: ID=", msg.Id, ", len=", msg.DataLen, ", data=", string(msg.Data))
}

time.Sleep(1*time.Second)
}
}
```

### Zinx配置文件
```json
{
"Name":"Zinx Game",
"Host":"0.0.0.0",
"TcpPort":8999,
"MaxConn":3000,
"WorkerPoolSize":10
}
```
`Name`:服务器应用名称
`Host`:服务器IP
`TcpPort`:服务器监听端口
`MaxConn`:允许的客户端链接最大数量
`WorkerPoolSize`:工作任务池最大工作Goroutine数量

###I.服务器模块Server
```go
func NewServer () ziface.IServer
```
创建一个Zinx服务器句柄,该句柄作为当前服务器应用程序的主枢纽,包括如下功能:

####1) 开启服务
```go
func (s *Server) Start()
```
####2) 停止服务
```go
func (s *Server) Stop()
```
####3) 运行服务
```go
func (s *Server) Serve()
```
####4) 注册路由
```go
func (s *Server) AddRouter (msgId uint32, router ziface.IRouter)
```
####5) 注册链接创建Hook函数
```go
func (s *Server) SetOnConnStart(hookFunc func (ziface.IConnection))
```
####6) 注册链接销毁Hook函数
```go
func (s *Server) SetOnConnStop(hookFunc func (ziface.IConnection))
```
###II. 路由模块

```go
//实现router时,先嵌入这个基类,然后根据需要对这个基类的方法进行重写
type BaseRouter struct {}

//这里之所以BaseRouter的方法都为空,
// 是因为有的Router不希望有PreHandle或PostHandle
// 所以Router全部继承BaseRouter的好处是,不需要实现PreHandle和PostHandle也可以实例化
func (br *BaseRouter)PreHandle(req ziface.IRequest){}
func (br *BaseRouter)Handle(req ziface.IRequest){}
func (br *BaseRouter)PostHandle(req ziface.IRequest){}
```


###III. 链接模块
####1) 获取原始的socket TCPConn
```go
func (c *Connection) GetTCPConnection() *net.TCPConn
```
####2) 获取链接ID
```go
func (c *Connection) GetConnID() uint32
```
####3) 获取远程客户端地址信息
```go
func (c *Connection) RemoteAddr() net.Addr
```
####4) 发送消息
```go
func (c *Connection) SendMsg(msgId uint32, data []byte) error
func (c *Connection) SendBuffMsg(msgId uint32, data []byte) error
```
####5) 链接属性
```go
//设置链接属性
func (c *Connection) SetProperty(key string, value interface{})

//获取链接属性
func (c *Connection) GetProperty(key string) (interface{}, error)

//移除链接属性
func (c *Connection) RemoveProperty(key string)
```


---
###关于作者:

作者:`Aceld(刘丹冰)`
简书号:`IT无崖子`

mail: [danbing.at@gmail.com](mailto:danbing.at@gmail.com)
github: [https://github.com/aceld](https://github.com/aceld)
原创书籍gitbook: [http://legacy.gitbook.com/@aceld](http://legacy.gitbook.com/@aceld)


>**原创声明:未经作者允许请勿转载,或者转载请注明出处!**

分享一个纯 Go 编写的内嵌型 KV 数据库 NutsDB,支持事务以及多种数据结构

开源程序xujiajun 发表了文章 • 0 个评论 • 398 次浏览 • 2019-03-11 21:52 • 来自相关话题

大家好,分享一个我最近开源的用纯GO编写的内嵌型数据库。是对nosql的一个阶段性的实践。 ## Feature: * 支持 ACID 事务 (从v 0.3.0开始) * 支持基本的 ...查看全部
大家好,分享一个我最近开源的用纯GO编写的内嵌型数据库。是对nosql的一个阶段性的实践。


## Feature:

* 支持 ACID 事务 (从v 0.3.0开始)
* 支持基本的 Put、Delete、Get 操作
* 支持前缀扫描
* 支持范围扫描
* 除了基本的 String,还支持多种数据结构类似Redis的APi,如列表(list)、集合(set)、有序集合(sorted set)。

## 项目地址

https://github.com/xujiajun/nutsdb

> 欢迎大家给我提issue、star关注、提交PR。

小米正式开源 Istio 管理面板 Naftis

开源程序sevennt 发表了文章 • 2 个评论 • 1169 次浏览 • 2018-10-25 15:42 • 来自相关话题

近年来服务网格(Service Mesh)已成为各大公司关注重点,各大公司纷纷开始调研 Service Mesh 相关架构。作为 Service Mesh 中的佼佼者,Istio 诞生之初就已吸引众多目光。 作为基础设施层,Istio ...查看全部
近年来服务网格(Service Mesh)已成为各大公司关注重点,各大公司纷纷开始调研 Service Mesh 相关架构。作为 Service Mesh 中的佼佼者,Istio 诞生之初就已吸引众多目光。

作为基础设施层,Istio 有优秀的服务治理能力。但使用 Istio 进行服务治理时,开发者需通过 istioctl 或 kubectl 工具在终端中进行操作,这种方式目前存在一些问题,举例如下:

1. Istio 要求用户熟练掌握 istioctl 工具的数百种指令,有较高的学习成本。
2. Istio 进行服务治理时需要的 yaml 配置文件的数量非常庞大,如何配置和管理这些配置文件,也是个难题。
3. Istio 的 istioctl 工具没有用户权限的约束,存在一定安全隐患,无法适应大公司严格的权限管理需求。
4. Istio 的 istioctl 工具不支持任务回滚等需求,在执行任务出错的情况下,无法快速回滚到上一个正确版本。

为了解决这些问题,小米信息部武汉研发中心为 Istio 研发出了一套友好易用的 dashboard —— Naftis 。

> Naftis 意为水手,和 Istio (帆船)意境契合。作为 dashboard , Naftis 能使用户像水手一样熟练掌控和管理 Istio 。

[https://github.com/xiaomi/naftis](https://github.com/xiaomi/naftis)

Naftis 通过任务模板的方式来帮助用户更轻松地执行 Istio 任务。用户可以在 Naftis 中定义自己的任务模板,并通过填充变量来构造单个或多个任务实例,从而完成各种服务治理功能。

Naftis 提供了如下特性:

- 集成了一些常用的监控指标,包括 40X、50X 错误趋势等。
- 提供了可定制的任务模板的支持。
- 支持回滚指定某一任务。
- 提供了 Istio 状态诊断功能,可实时查看 Istio 的 Services 和 Pod 状态。
- 开箱即用,通过 kubectl 指令一键部署。

## 依赖

目前 Naftis 仅支持 Kubernetes,不支持其他容器调度平台。

- Istio > 1.0
- Kubernetes>= 1.9.0Jf
- HIUI >= 1.0.0

Naftis 后端采用 Go 编写,通过 Kubernetes 和 Istio 的 CRD 接口对 Istio 资源进行操作;
前端则采用了同样由小米开源的基于 React 的前端组件 HIUI,HIUI 简洁优雅,有一定 React 基础的前端开发者能迅速上手:

[https://github.com/xiaomi/hiui](https://github.com/xiaomi/hiui)

## 快速开始

```bash
kubectl create namespace naftis && kubectl apply -n naftis -f mysql.yaml && kubectl apply -n naftis -f naftis.yaml

# 通过端口转发的方式访问 Naftis
kubectl -n naftis port-forward $(kubectl -n naftis get Pod -l app=naftis -ui -o jsonpath='{.items[0].metadata.name}') 8080:80 &

# 打开浏览器访问 http://localhost:8080,默认用户名和密码分别为 admin、admin。
```

## 详细的部署流程

### Kubernetes 集群内运行

```bash
# 创建 Naftis 命名空间
$ kubectl create namespace naftis

# 确认 Naftis 命名空间已创建
$ kubectl get namespace naftis
NAME STATUS AGE
naftis Active 18m

# 部署 Naftis MySQL 服务
$ kubectl apply -n naftis -f mysql.yaml

# 确认 MySQL 已部署
$ kubectl get svc -n naftis
NAME READY STATUS RESTARTS AGE
naftis-mysql-c78f99d6c-kblbq 1/1 Running 0 9s
naftis-mysql-test 1/1 Running 0 10s

# 部署 Naftis API和 UI 服务
$ kubectl apply -n naftis -f naftis.yaml

# 确认 Naftis 所有的服务已经正确定义并正常运行中
$ kubectl get svc -n naftis
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
naftis-api ClusterIP 10.233.3.144 50000/TCP 7s
naftis-mysql ClusterIP 10.233.57.230 3306/TCP 55s
naftis-ui LoadBalancer 10.233.18.125 80:31286/TCP 6s

$ kubectl get Pod -n naftis
NAME READY STATUS RESTARTS AGE
naftis-api-0 1/2 Running 0 19s
naftis-mysql-c78f99d6c-kblbq 1/1 Running 0 1m
naftis-mysql-test 1/1 Running 0 1m
naftis-ui-69f7d75f47-4jzwz 1/1 Running 0 19s

# 端口转发访问 Naftis
kubectl -n naftis port-forward $(kubectl -n naftis get Pod -l app=naftis-ui -o jsonpath='{.items[0].metadata.name}') 8080:80 &

# 打开浏览器,访问 http://localhost:8080 即可。默认用户名和密码分别为 admin、admin。
```

### 本地运行

#### 数据移植

```bash
# 执行 sql语句
mysql> source ./tool/naftis.sql;

# 将 in-local.toml 中的数据库的 DSN 配置替换成本地数据库实例的 DSN。
```

### 启动 API 服务

- Linux

```bash
make build && ./bin/naftis-api start -c config/in-local.toml
```



```bash
./run
```

- Mac OS

```bash
GOOS=darwin GOARCH=amd64 make build && ./bin/naftis-api start -c config/in-local.toml
```



```bash
GOOS=darwin GOARCH=amd64 ./run
```

#### 配置 Nginx 代理

```bash
cp tool/naftis.conf /naftis.conf
# 酌情修改 naftis.conf 文件并 reload nginx
```

#### 启动前端 Node 代理

```bash
cd src/ui
npm install
npm run dev

# 打开浏览器访问 http://localhost:5200。
```

## 预览

### dashboard

dashboard 页面集成了一些常用的图表,比如请求成功率、4XX 请求数量等。
![集成了一些常用的图表,比如请求成功率、4XX请求数量等](https://ww1.sinaimg.cn/large/889d06cbgy1fwjbpzdo15j20zk0j3gp9.jpg)

### 服务管理

#### 服务详情

服务详情页面可以查看查看已部署到 Kubernetes 中服务信息。
![查看已部署到k8s中服务信息](https://ww1.sinaimg.cn/large/889d06cbgy1fwjbpzlkpaj20zk0sn798.jpg)

#### 服务 Pod 和拓扑图等

服务详情页面可以查看指定服务 Pod 和拓扑图等信息。
![ Services - Pod ](https://ww1.sinaimg.cn/large/889d06cbgy1fwjbpzdu3kj20zk0tg78s.jpg)

### 任务模板管理

#### 任务模板列表

任务模板列表也可以查看已经添加好的任务模板卡片列表。
![任务模板列表](https://ww1.sinaimg.cn/large/889d06cbgy1fwjbpze4dxj20zk0hqad7.jpg)

#### 查看指定模板

点击“查看模板”可以查看指定模板信息。
![查看指定模板](https://ww1.sinaimg.cn/large/889d06cbgy1fwjbpzgotbj20zk0io417.jpg)

#### 新增模板

点击“新增模板”可以向系统中新增自定义模板。
![新增模板](https://ww1.sinaimg.cn/large/889d06cbgy1fwjbpzk3gkj20zk0ion0d.jpg)

#### 创建任务

初始化变量值。
![创建任务-第一步](https://ww1.sinaimg.cn/large/889d06cbgy1fwjbpzcuinj20zk0iota3.jpg)

确认变量值。
![创建任务-第二步](https://ww1.sinaimg.cn/large/889d06cbgy1fwjbpzibuqj20zk0ioabm.jpg)

提交创建任务的分步表单。
![创建任务-第三步](https://ww1.sinaimg.cn/large/889d06cbgy1fwjbpzifhzj20zk0ix75v.jpg)

#### Istio 诊断

Istio 诊断页面可以查看 Istio Service 和 Pod 状态。
![查看 Istio 状态](https://ww1.sinaimg.cn/large/889d06cbgy1fwjbpzmzflj20zk0v5dlf.jpg)

## Docker 镜像

Naftis 的 API 和 UI 镜像已经发布到 Docker Hub 上,见 [api](https://hub.docker.com/r/sevennt/naftis-api/) 和 [ui](https://hub.docker.com/r/sevennt/naftis-ui/)。

## 开发者指南

### 获取源码

```bash
go get github.com/xiaomi/naftis
```

### 配置环境变量

将下述环境变量添加到 `~/.profile` 。我们强烈推荐通过 [autoenv](https://github.com/kennethreitz/autoenv) 来配置环境变量。

```bash
# 根据环境改变 GOOS 和 GOARCH 变量
export GOOS="linux" # 或替换成 "darwin"
export GOARCH="amd64" # 或替换成 "386"

# 把 USER 修改成你自己的 DockerHub 用户名
export USER="sevennt"
export HUB="docker.io/$USER"
```

如果你使用 [autoenv](https://github.com/kennethreitz/autoenv),则输入 `cd .` 来使环境变量生效。

### Go 依赖

我们目前使用 [dep](https://github.com/golang/dep) 管理依赖。

```bash
# 安装 dep
go get -u github.com/golang/dep
dep ensure -v # 安装 Go 依赖
```

### 代码风格

- [Go](https://github.com/golang/go/wiki/CodeReviewComments)
- [React](https://standardjs.com/)

## 其他指令

```bash
make # 编译所有 targets

make build # 编译 Go 二进制文件、前端静态资源、Kubernetes清单
make build.api # 编译 Go 二进制文件
make build.ui # 编译前端静态资源
make build.manifest # 编译Kubernetes清单

make fmt # 格式化 Go 代码
make lint # lint Go 代码
make vet # vet Go 代码
make test # 运行测试用例
make tar # 打包成压缩文件

make docker # 编译 docker 镜像
make docker.api # 编译后端 docker 镜像
make docker.ui # 编译前端 docker 镜像
make push # 把镜像推送到 Docker Hub

./bin/naftis-api -h # 显示帮助信息
./bin/naftis-api version # 显示版本信息

./tool/genmanifest.sh # 本地渲染Kubernetes部署清单
./tool/cleanup.sh # 清理已部署的 Naftis
```

## 其他

Naftis 目前已在 Github 开源 ,目前功能还比较薄弱,希望更多志同道合的朋友一起参与进来共同完善落地 Istio 生态。

[https://github.com/xiaomi/naftis](https://github.com/xiaomi/naftis)

开源高性能 web 缓存服务器 nuster

开源程序kehokoduru 发表了文章 • 0 个评论 • 1647 次浏览 • 2018-02-22 11:09 • 来自相关话题

nuster, 一个基于 HAProxy 的高性能 web 缓存服务器 。 完全兼容 HAProxy,并且利用 HAProxy 的 ACL 功能来提供非常细致的缓存规则。 项目地址 https://github.com/jiangwe ...查看全部
nuster, 一个基于 HAProxy 的高性能 web 缓存服务器 。 完全兼容 HAProxy,并且利用 HAProxy 的 ACL 功能来提供非常细致的缓存规则。

项目地址 https://github.com/jiangwenyuan/nuster

可以根据 url, path, query, header, cookie,请求速率等等来动态生成缓存,并设置有效期。支持 purge,支持前后端 HTTPS。

* 完全兼容 HAProxy,支持所有 HAProxy 的特性和功能
* 强大的动态缓存功能
* 根据 HTTP method, uri, path, query, header, cookie 等等进行缓存
* 根据 HTTP 请求和响应内容等等进行缓存
* 根据环境变量服务器状态等等进行缓存
* 根据 SSL 版本, SNI 等等进行缓存
* 根据连接数量,请求速率等等进行缓存
* 等等
* 非常快
* 删除缓存
* 前后端 HTTPS
* HTTP 压缩
* HTTP 重写重定向
* 等等

性能

非常快, 单进程模式下是 nginx 的 3 倍,多进程下 nginx 的 2 倍,varnish 的 3 倍。

详见[https://github.com/jiangwenyuan/nuster/wiki/Web-cache-server-performance-benchmark:-nuster-vs-nginx-vs-varnish-vs-squid](https://github.com/jiangwenyuan/nuster/wiki/Web-cache-server-performance-benchmark:-nuster-vs-nginx-vs-varnish-vs-squid)