Go:Unix域套接字

关于同一个Linux主机上的进程之间的进程间通信(IPC)方式,有多个选择:例如FIFO、管道、共享内存、套接字等等。最有趣的选项之一是Unix域套接字,它结合了socket的API和其他更高性能单机方法。

这篇文章展示了一些在Go中使用Unix域套接字的简单例子,并与TCP回环套接字的基准测试进行对比。

Unix域套接字(unix domain sockets-UDS)

Unix域套接字(UDS)已经有很长的历史了,可追溯到20世纪80年代的原始BSD套接字规范。维基百科中的定义:

Unix域套接字或IPC套接字(进程间通信套接字)是一个数据通信端点,用于在同一主机操作系统上执行进程之间数据交换。

UDS支持流(类似TCP)和数据报(类似UDP);本文主要关注流API。使用UDS来实现进程间通信和使用回环接口(localhost或127.0.0.1)基于常规的TCP套接字类似,但有一个关键不同点:性能。虽然TCP环回接口也可以跳过完整的TCP/IP网络堆栈的一些复杂性,但它保留了许多其他的功能(例如ack、TCP流控制等)。这些是为可靠的跨主机通信而设计的,但在单机上,它们是不必要的负担。本文将探讨UDS的一些性能优势。

还有一些额外的差异。例如,UDS使用文件系统中的路径作为其地址,我们可以使用目录和文件权限来控制对套接字的访问,从而简化认证过程。在这里就不列出所有的区别了;如需更多信息,请查看维基百科和Beej的UNIX IPC指南等资源。

当然,与TCP套接字相比,UDS最大的缺点是单机限制。对于使用TCP套接字编写的代码,我们只需要将地址从本地更改为远程主机IP地址,就可正常工作。也就是说,UDS的性能优势非常显著,而且API与TCP套接字非常相似,因此编写同时支持这两种类型的代码(单机使用UDS,远程IPC使用TCP)非常容易。

在Go中使用Unix domain socket

我们从一个简单例子开始,使用Go监听Unix域套接字:

package main

import (
    "io"
    "log"
    "net"
    "os"
)

const SockAddr = "/tmp/echo.sock"

func echoServer(c net.Conn) {
    log.Printf("Client connected [%s]", c.RemoteAddr().Network())
    io.Copy(c, c)
    c.Close()
}

func main() {
    if err := os.RemoveAll(SockAddr); err != nil {
        log.Fatal(err)
    }

    l, err := net.Listen("unix", SockAddr)
    if err != nil {
        log.Fatal("listen error:", err)
    }
    defer l.Close()

    for {
        // Accept new connections, dispatching them to echoServer
        // in a goroutine.
        conn, err := l.Accept()
        if err != nil {
            log.Fatal("accept error:", err)
        }

        go echoServer(conn)
    }
}

UDS通过文件系统中的路径进行识别;对于服务器端代码,我们使用/tmp/echo.sock。以上代码从删除这个文件开始,如果文件存在说明已经有服务在监听会报错。

当服务器关闭时,表示套接字的文件可以保留在文件系统中,除非服务停止后自动清理。如果我们用相同的套接字路径重新运行另一个服务器,会得到以下错误:

2021/10/20 23:23:24 listen error:listen unix /tmp/echo.sock: bind: address already in use

为了防止这种情况,服务端首先删除套接字文件(如果存在的话)。 运行服务端代码,我们可以使用Netcat与它进行交互,使用-U标志请求连接到UDS:

$ nc -U /tmp/echo.sock

连接上以后无论你输入什么,服务器都会返回输入的内容。按Ctl+c终止会话。或者,我们可以在Go中编写一个简单的客户端,它连接到服务器,发送消息,等待响应并退出。客户端代码如下:

package main

import (
    "io"
    "log"
    "net"
    "time"
)

func reader(r io.Reader) {
    buf := make([]byte, 1024)
    n, err := r.Read(buf[:])
    if err != nil {
        return
    }
    println("Client got:", string(buf[0:n]))
}

func main() {
    c, err := net.Dial("unix", "/tmp/echo.sock")
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()

    go reader(c)
    _, err = c.Write([]byte("hi"))
    if err != nil {
        log.Fatal("write error:", err)
    }
    reader(c)
    time.Sleep(100 * time.Millisecond)
}

我们可以看到,编写UDS服务器和客户端与编写常规套接字服务和客户端非常相似。唯一的不同就是必须传入“unix”作为网络类型参数到net.Listen和net.Dial中。其余代码都是一样的。显然,这使得编写通用的服务端和客户端代码变得非常容易,这些代码与所使用的套接字的实际类型无关。

基于UDS实现HTTP和RPC协议

网络协议如HTTP和各种形式的RPC,并不特别关心网络栈的底层是如何实现的,只要能对一些功能保持一致就可以。
Go标准库自带rpc包,使得实现RPC服务器和客户端变得非常简单。下面是一个使用UDS实现简单的服务器:

const SockAddr = "/tmp/rpc.sock"

type Greeter struct {
}

func (g Greeter) Greet(name *string, reply *string) error {
    *reply = "Hello, " + *name
    return nil
}

func main() {
    if err := os.RemoveAll(SockAddr); err != nil {
        log.Fatal(err)
    }

    greeter := new(Greeter)
    rpc.Register(greeter)
    rpc.HandleHTTP()
    l, e := net.Listen("unix", SockAddr)
    if e != nil {
        log.Fatal("listen error:", e)
    }
    fmt.Println("Serving...")
    http.Serve(l, nil)
}

注意,我们使用的是rpc服务器的HTTP版本。它用HTTP包注册一个HTTP处理程序,实际的服务使用标准HTTP.serve完成。这里的网络栈看起来像这样:



这里提供了一个可以连接到上面所示服务器的RPC客户端。它使用标准的rpc.Client.Call方法连接到服务器。

package main

import (
    "fmt"
    "log"
    "net/rpc"
)

func main() {
    client, err := rpc.DialHTTP("unix", "/tmp/rpc.sock")
    if err != nil {
        log.Fatal("dialing:", err)
    }
    // Synchronous call
    name := "Joe"
    var reply string
    err = client.Call("Greeter.Greet", &name, &reply)
    if err != nil {
        log.Fatal("greeter error:", err)
    }
    fmt.Printf("Got '%s'\n", reply)
}

回环套接和Unix域套接字的基准测试对比

基准测试是很难的标准到,所以这里的对比结果仅供参考。这里运行两种基准测试:一种是延迟,另一种是吞吐量。

延时的基准测试代码在这里使用-help命令行参数查看如何使用,代码非常简单。其思想是在服务端和客户端口之间发送一个小数据包(默认为128字节)。客户端测量发送一条这样的消息和接收一条消息所需的时间,并通过发送多次取平均值。

在我的机器上,我看到TCP环回套接字的平均延迟为3.6微秒,UDS的平均延迟为2.3微秒。

吞吐量/带宽基准测试在概念上比延迟基准简单。服务器侦听套接字并获取它能获得的所有数据(然后丢弃它)。客户端发送大数据包(数百KB或更多),并测量每个数据包发送所需的时间;发送是同步完成的,客户端希望在单个调用中发送整个消息,所以如果包的大小足够大,可近似带宽。

显然,吞吐量度量对于较大的消息更具有代表性。我尝试增加,直到吞吐量提升逐渐减少。

对于更小的数据包,我认为UDS优于TCP: 在512K包中,分别为10GB /秒和9.4 GB/秒。对于更大的数据包(16-32 MB),差异变得微不足道(两者都以大约13 GB/秒的速度减少)。有趣的是,对于一些数据包(比如64K), TCP套接字在我的机器上更占优势。

对于小的数据包,UDS的性能比TCP的块很多超过2倍以上。在大多数情况下,我认为延迟更重要—它们更适用于衡量RPC服务器和数据库性能。在某些情况下,比如在套接字上的视频流或其他“大数据”,您可能需要仔细选择包的大小,以优化所使用的特定机器的性能。

Unix域套接字在实际项目中使用

我很好奇在真实的Go项目中是否真的使用了UDS。没错!在GitHub上搜索了几分钟,很快发现用go写的开源云基础设施项目都使用了UDS,例如:runc、moby (Docker)、k8s、lstio—几乎每个项目都是我们熟悉的。

正如基准测试所证明的,当客户端和服务器都在同一台主机上时,使用UDS具有显著的性能优势。而且UDS和TCP套接字的API非常相似,因此支持两者可互换的成本非常低。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容