Golang Context 的实现

有这样一个场景: 父 goroutine 创建了多个子 goroutine 来处理某个请求,当这些子 goroutine 中任何一个出错的时候,我们希望所有的 goroutine 都停止。 该如何实现呢? 熟悉 Go 语言的可能首先想到用 context,而 context 主要是依靠 channel 来实现以上功能。 看了一下具体的实现,主要思想是: 每种类型的 ctx 都实现了 context.Context 接口的 Done() 函数 Done() <-chan struct{} 函数返回一个只读的 channel 而且没有地方向这个channel里写数据。所以直接调用这个只读channel会被阻塞。 一般通过搭配 select 来使用。一旦 channel 关闭,就会立即读出零值。 谁来关闭这个 channel 呢? 用户主动调用返回的 CancelFunc,或者 timeout 超时 另外,在使用上配合 select 语句阻塞处理 Done() 才能起到预期的效果。 下面举两个如何使用 context 的例子,第一个例子如下 func main() { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() go handle(ctx) // 等待3秒再结束(只是为了让 main 不提前 exit,与本文无关) select { case <- time.After(3*time.Second): fmt.Println("main", ctx.Err()) } } func handle(ctx context.Context) { select { case <-ctx.Done(): fmt.Println("goroutine ", ctx.Err()) // 处理请求,在这里我们用 time.After() 表示处理了多久, 也可以写成这样 // case ret, ok := <-req: case <-time.After(2*time.Second): fmt.Println("process request done") } } 在这个例子中,ctx 会在1秒后超时,而 goroutine 处理某个 request 需要 2 秒(在代码中用 time.After 代替真正的处理时间)。 ...

2021-03-28 · Me

Kubernetes 中的 DNS

该文件指定如何解析主机名 cat /etc/host.conf order hosts, bind multi on order bind,hosts 指定主机名查询顺序,这里规定先使用 DNS 来解析域名,然后再查询 /etc/hosts 文件(也可以相反) multi on 指 /etc/hosts 文件中的主机可以有多个地址 nospoof on 指不允许对该服务器进行IP地址欺骗

2021-03-10 · Me

Rust 的所有权和生命周期

所有权 在 Rust 中,heap 上的一块内存区域是一块 “值”,与之绑定的是一个变量,也就是说变量和值是绑定的,要注意这种绑定关系。 在任何时候,一个值只有一个对应的变量作为所有者。 理解了这些概念之后,再来看所有权和它的基本特性: Rust中的每个值都有一个对应的变量作为它的所有者; 在同一时间内,只有且仅有一个所有者; 当所有者离开自己的作用域时,它持有的值就会被释放掉。 所有权的转移 赋值即转移。 如下面的示例, fn test() { let v: Vec<u8> = vec![0;20]; let u = v } 在第二行,u 成为了内存中这个数组数据的所有者,当函数返回时,u 的作用域结束,这块内存随即被释放。 要想让 v 和 u 各自都拥有独立的数据,可以使用 v.clone() 函数, 注意,int, char 等基本类型,在赋值的时候等于自动调用了 clone,所以对于这些基本类型可以放心的像 C/C++ 语言那样使用。 所有权的借用 & 是一个在 C/C++ 和 Golang 中常见的符号,在 Rust 中,用在一个变量上是借用的意思,也就是说所有权不变。 官方文档用这样一个例子来说明借用 fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() } 最后一行的 s 是 s1 的引用,是对数据的借用,s1 仍然是数据的所有者,在 calculate_length() 返回之后,s 也会被销毁,但不影响原始数据。 ...

2021-02-28 · Me

Golang 读写锁的实现

type RWMutex struct { w Mutex // held if there are pending writers writerSem uint32 // semaphore for writers to wait for completing readers readerSem uint32 // semaphore for readers to wait for completing writers readerCount int32 // number of pending readers readerWait int32 // number of departing readers } writerSem 是写入操作的信号量 readerSem 是读操作的信号量 readerCount 是当前读操作的个数 readerWait 当前写入操作需要等待读操作解锁的个数 其中 semaphore 就是操作系统课程里面学到的信号量的概念。 读写锁的实现非常简单,源码在 /usr/local/go/src/sync/rwmutex.go 下,我们可以逐一分析它的各个函数 读者加读锁 首先是读锁,读者进入临界区之前,把 readerCount 加一, 如果这个值小于 0,则调用runtime_SemacquireMutex 把自己所在的 goroutine 挂起。 如果大于等于 0, 则加读锁成功 func (rw *RWMutex) RLock() { if atomic.AddInt32(&rw.readerCount, 1) < 0 { // A writer is pending, wait for it. runtime_SemacquireMutex(&rw.readerSem, false, 0) } } runtime_SemacquireMutex() 是一个运行时函数,实际调用的是在go/src/runtime/sema.go 中的函数 sync_runtime_SemacquireMutex(), 这个函数的具体实现不在本文讨论范围,目前只要知道这个函数实现了信号量的 P 操作,goroutine 在这个函数中挂起等待。 ...

2021-02-18 · Me

epoll 在 Golang net 库的使用

本文主要关注以下几个问题: Golang runtime 是怎么调用 epoll 的系统调用的 ? Golang net 库如何封装 epoll,使得开发者几乎不用直接操作 epoll ? C 如何调用 epoll 首先回顾一下用 C 语言怎么使用 epoll int s = socket(AF_INET, SOCK_STREAM, 0); bind(s...) listen(s...) int epfd = epoll_create(128); //创建eventpoll对象 ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET epoll_ctl(epfd, EPOLL_CTL_ADD, s, &ev);//注册事件 //轮询就绪事件 while(true){ //返回值n为就绪的事件数,events为事件列表 int n = epoll_wait(epfd, &events[0], len(events), 1000) for( i := 0; i < n; i++ ) { ev := &events[i] //处理事件 } } C 语言中调用 epoll 的方式比较底层,总的来说分下面三个步骤 epoll_create() 创建epoll对象 epoll_ctl() 注册套接字的事件 epoll_wait() 轮询是否有事件发生,并通过events参数返回就绪(触发)的事件列表 Go 如何调用 epoll 那么在 Golang 的 runtime 最终也是调用了这么底层的 glibc 库函数吗? Golang 是如何封装 epoll 的呢? 搜索之后在 runtime/sys_linux_amd64.s 文件下面找到了 epollctl() // func epollctl(epfd, op, fd int32, ev *epollEvent) int TEXT runtime·epollctl(SB),NOSPLIT,$0 MOVL epfd+0(FP), DI MOVL op+4(FP), SI MOVL fd+8(FP), DX MOVQ ev+16(FP), R10 MOVL $SYS_epoll_ctl, AX SYSCALL MOVL AX, ret+24(FP) RET 可见,Golang runtime 最后是直接用汇编调用 系统调用 epollctl,类似的,在同一个文件下也能找到 epoll create 和 wait 的汇编代码。 ...

2021-01-31 · Me

gRPC-go Server 端实现

在上一篇文章中,介绍了 grpc 建立 TCP 连接的过程,侧重点在 Client 端,而关于 Server 端建立 TCP 的过程相对是比较简单的。 Server端 listen on 本地端口,并且接收来自 client 的连接请求,一旦建立 TCP 连接后,接下来的步骤是什么呢? 建立 HTTP2 server,并收发数据。 本文尝试回答一下几个问题: Server 怎么利用 http2 的 stream 传输数据? 从 stream 里读的数据存放在哪? Stream 读到的数据如何传给用户 Server 要发送的数据又是从哪发送的? 创建 http2Server 首先从用户的代码入手,用户的代码最后会调用 grpcServer.Serve(lis), 稍微追踪几个函数就能发现调用链是 handleRawConn() 到 serveStreams()。 从 handleRawConn() 中我们发现 newHTTP2Transport 会创建一个新的 http2Server。 serveStreams() 中的 HandleStreams() 是 type ServerTransport interface 的一个函数,而 type http2Server struct 实现了这个接口。 值得注意的是,有两个结构体实现了 ServerTransport,分别是 transport/handler_server.go 的 serverHandlerTransport transport/http2_server.go 的 http2Server 一般我们在 main 函数中调用 grpcServer.Serve(lis) 的,最后都是 http2Server。 如果是 ServerHTTP() 则是第一个 serverHandlerTransport,所以这一个很少用,代码不用看。 ...

2021-01-18 · Me

Bittorrent 协议及工作原理

在 2000 年左右开始接触互联网的同学都应该记得用 BT 种子下载电影和小电影那段的时光。之前只是大概知道 BT 的工作原理,但并没有仔细研究过,所以一直很好奇。 随便在网上搜索下,可以知道 BT 大概是这样工作的: BitTorrent 协议把提供下载的文件虚拟分成大小相等的块,块大小必须为 2k 的整数次方,并把每个块的索引信息和 Hash 验证码 写入 .torrent 文件(即种子文件,也简称为“种子”)中,作为被下载文件的“索引”。 下载者要下载文件内容,需要先得到相应的 .torrent 文件,然后使用 BT 客户端软件进行下载。 下载时,BT 客户端首先解析 .torrent 文件得到 Tracker 地址,然后连接 Tracker 服务器。Tracker 服务器回应下载者的请求,提供下载者其他下载者(包括发布者)的 IP。或者,BT客户端也可解析 .torrent 文件得到 nodes 路由表,然后连接路由表中的有效节点,由网络节点提供下载者其他下载者的 IP。 torrent 文件包含了什么 根据 bittorrent.org官方文档,种子文件也被称为metainfo files, 主要包含以下信息: announce, The URL of the tracker. info, This maps to a dictionary. 所以种子文件就是告诉你,去 announce 这个地址找文件,具体文件信息包含在 info 里面。 Info 结构体有以下基本内容: name key maps to a UTF-8 encoded string. piece length maps to the number of bytes in each piece the file is split into. pieces maps to a string whose length is a multiple of 20. 其实就是文件被切成很多片,这个变量保存了所有片的 SHA1 值。 length - The length of the file, in bytes. 以上4个是最基本的结构体信息,只支持下载单个文件,如果是表示多个文件或文件夹,还需要增加一些额外信息,具体见官方文档。 ...

2020-10-24 · Me

Docker 的 privileged 模式

无论是 docker 启动一个 container 还是在 k8s 中 deploy 一个 Pod 都可以指定 privileged 参数,之前在 Pod 的 spec YAML file 也里曾经用过,但是一直没有仔细想过加上这个参数后有什么不一样,今天就来研究一下。 首先来看一个最直观的对比,先运行一个没有 privileged 的容器: $ docker run --rm -it ubuntu:18.04 bash root@e6f5f42c5b7e:/# ls /dev/ console core fd full mqueue null ptmx pts root@e6f5f42c5b7e:/# fdisk -l 再来看看如果加上了 privileged 会有什么不一样: $ docker run --rm -it --privileged ubuntu:18.04 bash root@8e28f79eec9e:/# ls /dev/ tty11 tty2 tty28 tty36 tty44 tty52 tty60 ... ... root@8e28f79eec9e:/# fdisk -l Disk /dev/loop0: 97.1 MiB, 101777408 bytes, 198784 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes 不止能看到设备文件,甚至还能 mount 宿主机的文件系统, ...

2020-10-18 · Me

Goroutine Pool 实现高并发

本文是读完 Handling 1 Million Requests per Minute with Go 之后,根据自己的理解,对文中提到的并发模型和实现再梳理一遍。 前言 假设有一个 http server 接收 client 发来的 request,如果用下面的这样的代码,会有什么问题呢? func payloadHandler(w http.ResponseWriter, r *http.Request) { // Go through each payload and queue items individually to be posted to S3 for _, payload := range content.Payloads { go payload.UploadToS3() // <----- DON'T DO THIS } } 显而易见,有 2 个问题: 接收一个 request 就开启一个 goroutine 处理,当 request 数量在短时间内暴增的话,光是 goroutine 的数量都足以让 server 崩溃。 每个 goroutine 都会与后端建立 TCP 连接,既耗费三次握手的时间,也会造成后端有大量 TCP 连接 所以,我们的目标是 没有蛀牙 可以控制 goroutine 的总数,方法是事先创建好一定数量的 goroutine,加入到一个 Pool 中 goroutine 启动时与后端建立 TCP 长连接,之后的通信都基于这个连接 根据原文作者给出的方法,整体的架构如下: ...

2020-10-13 · Me

gRPC-go 建立 TCP 连接的过程

首先看一个最简单的建立 client server 之间 gRPC 连接的代码,以这个代码为例,分析一下 TCP 是在何时建立的。 Server 端的代码相对来说很容易,一个最简单的 server 代码如下: func main() { lis, _ := net.Listen("tcp", fmt.Sprintf(":%d", 8080)) grpcServer := grpc.NewServer() protobuf.RegisterTestServer(grpcServer, &server{}) grpcServer.Serve(lis) } 在 grpc/server.go 中的 Serve() 函数调用了 lis.Accept() 并阻塞,当 client 端发来 TCP 请求时,Accept() 返回 Conn 结构,并开启 goroutine handleRawConn() 进行后续的处理。 就 TCP 来说,server 端的代码简单易懂,相比之下 client 端则不一样,一个基本的 Client 代码如下: func main() { conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure()) defer conn.Close() cli := protobuf.NewTestClient(conn) } 而要弄清楚 Client 端如何建立 TCP 却不容易,这是因为 grpc client 有 resolve DNS 以及做 load balancer 的功能,因此代码复杂很多。 从上面的代码不难看出肯定是在 Dial() 函数中建立的,它的具体实现是在 DialContext(), 返回 ClientConn 结构体指针,但是却看不到在哪建立了 TCP 链接,这是因为 TCP 链接是在一个 Goroutine 中异步建立的。如果想要 DialContext() 等连接建立完再返回,可以指定grpc.WithBlock()传入Options来实现。 ...

2020-10-11 · Me