导读2007 年,谷歌的一个团队在调研计算机编程语言时,发现有数百种可用于开发软件的语言,但没有一种能提供谷歌真正需要的特性。有些语言太过底层,有些又太过复杂,有些特性对他们来说反而会碍手碍脚。谷歌的开发人员想要的语言要简单到可以在几个小时内学...

20 个有用的 Go 语言微服务开发框架

2007 年,谷歌的一个团队在调研计算机编程语言时,发现有数百种可用于开发软件的语言,但没有一种能提供谷歌真正需要的特性。有些语言太过底层,有些又太过复杂,有些特性对他们来说反而会碍手碍脚。谷歌的开发人员想要的语言要简单到可以在几个小时内学会,但又要复杂到足以应付现代互联网的信息流。

他们的解决方案就是 Go 语言,这门语言对于 C 语言、Java 或 JavaScript 程序员来说——换句话说,就是所有的程序员——都很容易掌握。Go 语言提供了足够的特性来编写循环和代码块,但没有一个特性是需要很长的时间才能掌握的。内置的例程经过优化,可以从互联网获取数据和发送数据。其他的东西——即使是非常聪明的想法——都被排除在外。

Go 语言是微服务开发团队的一个很好的选择。即使你的项目可能不需要像谷歌那样向全世界提供电子邮件、地、搜索和其他云服务,但仍然可能需要向用户提供数十种不同的小型信息服务。

谷歌决定开源 Go 语言是一个明智之举。这门语言培育了数千个开源项目,这些项目为你的 Web 项目提供了开箱即用的构建块。这里列出了 20 个最有趣的开源项目,用于构建基于 Go 语言的微服务系统。从专注于消息传递、路由、错误处理或 API 的小工具包,到用于构建 MVC Web 应用程序的更全面的框架,你将发现,使用 Go 语言开发微服务有着非常丰富的选择。

Beego

https://beego.me/

Beego 框架提供了很多标准附加功能,例如全功能路由器和可用于执行 CRUD 操作的对象到数据库映射工具。Bee 是 Beego 爱好者的最爱,它是一个快速而强大的命令行工具,用于构建、更新、打包和部署应用程序。Bee 可以从模板生成源代码,并保持数据库的最新状态。

Buffalo

https://gobuffalo.io/en

Buffalo 团队需要能够将 Web 应用程序的所有部分组装在一起的东西,包括应用程序本身的一些设计。他们把能够安装在一起的很多部件叫作“生态系统”。如果你想要路由——很少有人不需要——Buffalo 就包含了 Gorilla/Mux。如果你需要模板,Buffalo 倾向于使用 Plush,而不是使用内置的 Go 语言模板机制。数据库连接模块集合 Pop 可以帮你将数据库信息转换为 Go 对象。你还可以找到连接数据库、处理 cookie 以及完成其他任务的标准方法。

Cobra

https://github.com/spf13/cobra

有时候,你只需要一个命令行界面。Cobra 提供了 CLI 的所有标准功能,因此你不必浪费时间实现代码来查找 -h 或 -help 标志。如果你的微服务需要对具有大量标志和其他功能的命令行调用做出响应,那么可以考虑集成 Cobra。

Docker

你当然可以在办公室服务器小黑屋里的裸机上运行微服务,但越来越多的人将他们的代码捆绑在 Docker 容器中,并将容器发到云端。小型的包更容易处理大量不同的代码块,当你对微服务架构的愿景要求你创建很多小的独立代码块时,这将是一项有价值的服务。

值得一提的是,Docker 是用 Go 语言开发的,尽管在部署 Docker 容器时你可能永远不会想到这一点。Docker 社区版是开源的,所以如果有必要,你可以参与其中,但很可能你只是将 Docker 作为部署微服务的工具。Go 语言爱好者之所以想要记住 Docker 是用 Go 语言开发的这一事实,是因为无处不在的 Docker 有力地证明了他们对这门语言的支持。

Echo

https://echo.labstack.com/

Echo 是一个极简框架,但它提供了很多最重要的组件。路由器可以将 URL 拆解,然后将拆解的各个部份转换为参数,因此你无需自行解析它们。然后,你可以混合使用身份验证、表单解析、压缩和合理性限制。你可以专注于从函数中返回正确的信息。

Errors

https://github.com/juju/errors

有时候,API 的用户会传递错误的参数。你可以自己处理这些参数,也可以把它们创给 Errors,这是一个可以自动执行大部分跟踪的库,方便进行调试。当发生错误时,Errors 会使用注释来详细说明出错的地方和位置。

Gin

https://github.com/gin-gonic/gin

Gin 是 Martini(https://github.com/go-martini/martini)的下一代框架。可以说,Gin 抛弃了那些额外的东西,专注于提供最有用的部分。花费大量时间构建 Node.js 微服务的开发人员会感到宾至如归。你可以实例化一个对象,然后附加函数来处理特定的调用,这样就可以创建一个微服务。Gin 负责处理路由,而你的函数处理业务逻辑。如果不去考虑标点符号,它的代码甚至看起来有点像 Node.js 代码。

Ginkgo

https://onsi.github.io/ginkgo/

测试可能是微服务开发当中最具挑战性的事情。Ginkgo 通过行为驱动测试扩展了标准 Go 发行版的内置测试机制。Ginkgo 提供了一种高级机制,用于指定函数或服务应该产生哪些结果。结果通常使用 Ginkgo 提供的 Gomega 匹配器(https://onsi.github.io/gomega/)进行评估,但如果你愿意,也可以使用不同的匹配器库。

Ginkgo 是一个全面的框架,提供了各种选项,用于设置测试数据、运行测试以及在事后释放测试数据。你只需要描述结果,然后让 Ginkgo 处理其他的事情。

Goa

https://github.com/goadesign/goa

如果你是一个曾经使用 Ruby 和 Praxis 框架的开发人员,或者是一个欣赏设计语言的强大力量人,那么你会在 Goa 中找到很多你喜欢的东西。你本身不需要编写 Go 代码。你使用 Goa DSL 为 API 编写设计规范,然后 Goa 将其转换为可执行的 Go 代码。DSL 针对微服务 API 进行了优化,并强制你的设计遵循标准的架构。

Gorilla

https://www.gorillatoolkit.org/

Gorilla 项目提供了一系列你需要的模块。Gorilla 的 Mux(https://www.gorillatoolkit.org/pkg/mux)路由器被很多其他框架使用,因为它太好用了。很多用户之所以使用 Gorilla,是因为 websocket(https://www.gorillatoolkit.org/pkg/websocket)。

Gotify

https://github.com/gotify/server

同步一组微服务所面临的一个挑战是建立有效的消息传递节点。Gotify 是一个简单的服务器,用于发送和接收消息,将你的微服务集合与持续存储的消息组合在一起。最有用的部分可能是它的 Web 接口,可帮助开发者应对最令人头疼的调试问题。

Hugo

https://github.com/gohugoio/hugo

Hugo 是一种静态站点生成器,可以用这个框架构建的微服务并不多,但如果网站只有有限的重复查询答案时,这是一个值得考虑的选项。Hugo 一次生成答案,然后可以重复使用。如果你已经已 HTML 格式提供答案,那么 Hugo 会非常有用。

Kite

https://github.com/koding/kite

如果你希望建立一个更加可控的服务群体,而不是通常的服务之间的互动,那么可以考虑一下 Kite。Kite 的目标是让微服务之间的通信协调变得更简单一些。来自 Kite 以外的 API 调用通过 websocket 进入,然后 Kite 使用更快、更低级别的套接字连接(基于 dnode)传递新消息。中间有一个叫作 Kontrol 的服务注册表和身份验证服务。如果你需要经常交换消息和协调很多的操作,那么在不同服务器之间添加这一层会让一切变得更快。

Logrus

https://github.com/sirupsen/logrus

要跟踪 API 的流入和流出数据和可能产生的错误,通常需要将日志写入文件中。这个过程可以很简单,比如在一个打开的文件中写入一行行的数据,但通常使用完整的日志框架会更好。Logrus 提供了格式化程序来标准化你的日志输出,并让后续的自动化日志文件分析变得更容易。不要尝试自己开发日志代码,使用像 Logrus 这样的库会事半功倍。

Nano

https://github.com/pasztorpisti/nano

构建一个微服务并不需要太多东西,Nano 就是一个极简主义的例子。它的实际代码不会超过 200 行,如果算上注释也只有 400 多行。你只需要几行代码就可以构建一个微服务——只包含处理请求所需的业务逻辑。这个框架还有一些其他不错的特性,例如与语言无关的 API 结构,这样你的 Go 代码就可以与使用其他语言开发的服务发生交互。它还提供了一个测试过程来,可以嵌入你的本地测试例程。简简单单,但却恰到好处。

Negroni

https://github.com/urfave/negroni

有些人看完 Martini 后,决定走一条更简单的道路。他们剥离了路由器和其他一些东西,创建了 Negroni,这是一个非常小型的工具,除了处理标准文件、自定义请求、从基本错误中恢复以及保留日志之外,它不会做更多的工作。如果你想要额外的东西,可以自己加入。Negroni 团队也提供了一系列与可以与 Negroni 一起使用的小型项目。

Renderer

https://github.com/thedevsaddam/renderer

在准备输出响应时,你需要获取数据并将其插入到模板中。Renderer 提供了各种输出格式(JSON、JSONP、XML、YAML、HTML、文件)和一个漂亮、快速和标准的模板引擎。

Revel

https://revel.github.io/

Revel 借鉴了 Webpack 的一个简洁的特性,这让 Revel 看起来就像一个 IDE 一样,或者至少是 IDE 的一部分,每当你对代码做出更改,它会持续地重新构建你的项目。当你保存修改后,Revel 会检测到更改,然后就编译代码,如果没有编译错误,就启动应用程序。因此,Revel 服务器会自动部署修改的码——在桌面上进行开发时这项功能非常好用,或许对于生产环境代码部署来说也是有点诱人的。

这个框架本身功能齐全,它提供了模板、缓存、验证和过滤器。如果你正在构建很多微服务,它还提供了一个模块系统,让你可以在项目之间共享一些 MVC 组件。

Testify

https://github.com/stretchr/testify

使用断言的最简单方法之一是使用 Testify,它是一个 Go 语言项目,还提供了模拟工具,用于快速测试大型微服务的各个部分。只需要几行代码就编写一些基本测试用例。

Tollbooth

https://github.com/didip/tollbooth

在你 API 之后,当然希望来自世界各个角落的人都可以调用它。但当你的服务器发生熔断,或者你看一看为了获得弹性而为云托管账户支付的费用时,你可能会改变主意。Tollbooth 是一个用于限制传入请求的轻量级系统。限制前门的流量就等于减少了对管道中微服务或数据库的需求,让一切保持运行顺畅。

不使用框架

你只需从头开始编写 Go 代码,不需要导入任何依赖项或者实例化任何控制对象。使用 Go 语言创建微服务其实很容易,因为它已经内置了很多基本代码。这就是为什么只用几百行代码就可以构建出像 Nano 这样的框架。

监听套接字、解压缩 HTTP 请求等工作都是通过标准库完成的。虽然框架提供了一些额外的功能,但很多时候如果你需要一个非常基本的微服务,就不需要用到框架。太多的“附加功能”可能反而会妨碍你,而且 Go 开发人员可能会说,太多的依赖反而让 Go 语言变得更复杂。

英文原文:https://www.infoworld.com/article/3326530/google-go/20-go-projects-for-mastering-microservices.html

Golang实现单机百万长连接服务 - 美图的三年优化经验

导读:美长连接服务历时三年,在内存优化上积累比较丰富的实践经验,本文将会介绍我们团队这些年在内存优化道路上做的一些尝试。

作者简介:王鸿佳,系统研发工程师,现任职于美公司,主要从事通讯及存储相关领域的研发。参与了通用长连接通道、美推送、分布式数据库(Titan 已开源)、路由分发器等项目研发。对基础研发技术及开源项目有浓厚的兴趣。

随着科技的飞速发展,技术的日新月异,长连接的运用场景日益增多。不仅在后端服务中被广泛运用,比较常见的有数据库的访问、服务内部状态的协调等,而且在 App 端的消息推送、聊天信息、直播弹字幕等场景长连接服务也是优选方案。长连接服务的重要性也在各个场合被业界专家不断提及,与此同时也引起了更为广泛地和讨论,各大公司也开始构建自己的长连接服务。

美公司于2016 年初开始构建长连接服务,与此同时, Go 在编程语言领域异军突起,考虑到其丰富的编程库,完善的工具链,简单高效的并发模型等优势,使我们最终选择 Go 去作为实现长连接服务的语言。在通信协议的选择上,考虑到 MQTT 协议的轻量、简单、易于实现的优点,选择了 MQTT 协议作为数据交互的载体。其整体的架构会在下文中做相应地介绍。

美长连接服务(项目内部代号为bifrost )已经历时三年,在这三年的时间里,长连接服务经过了业务的检验,同时也经历了服务的重构,存储的升级等,长连接服务从之前支持单机二十几万连接到目前可以支撑单机百万连接。在大多数长连接服务中存在一个共性问题,那就是内存占用过高,我们经常发现单个节点几十万的长连接,内存却占用十几G 甚至更多,有哪些手段能降低内存呢?

本文将从多个角度介绍长连接服务在内存优化路上的探索,首先会先通过介绍当前服务的架构模型,Go 语言的内存管理,让大家清晰地了解我们内存优化的方向和的重要数据。后面会重点介绍我们在内存优化上做的一些尝试以及具体的优化手段,希望对大家有一定的借鉴意义。

一个好的架构模型设计不仅能让系统有很好的可扩展性,同时也能在服务能力上有很好的体现。除此之外,在设计上多考虑数据的抽象、模块的划分、工具链的完善,这样不仅能让软件具有更灵活的扩展能力、服务能力更高,也提高系统的稳定性和健壮性以及可维护性。

在数据抽象层面抽象pubsub 数据集合,用于消息的分发和处理。模块划分层面我们将服务一分为三:内部通讯(grpcsrv)、外部服务(mqttsrv)、连接管理(session)。工具链的方面我们构建了自动化测试,系统 mock ,压测工具。美长连接服务架构设计如下:

Golang实现单机百万长连接服务 - 美的三年优化经验

一架构

从架构中我们可以清晰地看到由7 个模块组成,分别是:conf 、grpcsrv 、mqttsrv、session、pubsub、packet、util ,每个模块的作用如下:

conf :配置管理中心,负责服务配置的初始化,基本字段。

grpcsrv :grpc 服务,集群内部信息交互协调。

mqttsrv :mqtt 服务,接收连接,同时支持单进程多端口 MQTT 服务。

session :会话模块,管理状态变化,MQTT 信息的收发。

pubsub :订阅模块,按照 Topic 维度保存 session 并 Topic 通知给 session。

packet:协议解析模块,负责 MQTT 协议包解析。

util :工具包,目前集成监控、日志、grpc 、调度上报四个子模块。

众所周知,Go 是一门自带垃圾回收机制的语言,内存管理参照 tcmalloc 实现,使用连续虚拟地址,以页( 8k )为单位、多级缓存进行管理。针对小于16 byte 直接使用Go的上下文P中的mcache分配,大于 32 kb 直接在 mheap 申请,剩下的先使用当前 P 的 mcache 中对应的 size class 分配 ,如果 mcache 对应的 size class 的 span 已经没有可用的块,则向 mcentral 请求。如果 mcentral 也没有可用的块,则向 mheap 申请,并切分。如果 mheap 也没有合适的 span,则向操作系统申请。

Go 在内存统计方面做的也是相当出色,提供细粒度的内存分配、GC 回收、goroutine 管理等统计数据。在优化过程中,一些数据能帮助我们发现和分析问题,在介绍优化之前,我们先来看看哪些参数需要,其统计参数如下:

go_memstats_sys_bytes :进程从操作系统获得的内存的总字节数 ,其中包含 Go 运行时的堆、栈和其他内部数据结构保留的虚拟地址空间。

go_memstats_heap_inuse_bytes:在 spans 中正在使用的字节。其中不包含可能已经返回到操作系统,或者可以重用进行堆分配,或者可以将作为堆栈内存重用的字节。

go_memstats_heap_idle_bytes:在 spans 中空闲的字节。

go_memstats_stack_sys_bytes:栈内存字节,主要用于 goroutine 栈内存的分配。

在内存监控中根据Go 将堆的虚拟地址空间划分为 span ,即对内存8K或更大的连续区域进行统计。span 可能处于以下三种状态之一 :

此外有一部分统计没有从堆内存中分配的运行时内部结构(通常因为它们是实现堆的一部分),与堆栈内存不同,分配给这些结构的任何内存都专用于这些结构,这些主要用于调试运行时内存开销。

虽然Go 拥有了丰富的标准库、语言层面支持并发、内置runtime,但相比C/C++ 完成相同逻辑的情况下 Go 消耗内存相对增多。在程序的运行过程中,它的 stack 内存会随着使用而自动扩容,但在 stack 内存回收采用惰性回收方式,一定程度的导致内存消耗增多,此外还有GC 机制也会带来额外内存的消耗。

Go 提供了三种内存回收机制:定时触发,按量触发,手动触发。在内存垃圾少量的情况下,Go 可以良好的运行。但是无论采用哪种触发方式,由于在海量用户服务的情况下造成的垃圾内存是巨大的,在 GC 执行过程中服务都会感觉明显的卡顿。这些也是目前长连接服务面对的难题,在下文中我将会逐一介绍我们如何减少和解决问题的产生的具体实践。

在了解架构设计、Go 的内存管理、基础监控后,相信大家已经对当前系统有了一个大致的认识,先给大家展示一下内存优化的成果,下表一是内存优化前后的对比表,在线连接数基本相同的情况下,进程内存占用大幅度降低,其中 stack 申请内存降低约 5.9 G,其次 heap 使用内存降低 0.9 G,other 申请内存也小幅下降。那么我们是如何做到内存降低的呢?那接下来我将会把我们团队关于进行内存优化的探索和大家聊一聊。


优化前

优化后

在线链接数

225 K

225 K

进程占用内存

13.4 G

4.7 G

heap 使用内存

5.2 G

3.4 G

stack 申请内存

7.25 G

1.02 G

other 申请内存

0.9 G

0.37 G

表一内存优化前后的对比表

备注:进程占用内存≈ 虚拟内存- 未归还内存

在优化前随机抽取线上一台机器进行分析内存,通过监控发现当前节点进程占用虚拟内存为22.3 G,堆区使用的内存占用 5.2 G ,堆区未归还内存为 8.9 G,栈区内存为 7.25 G,其它约占用 0.9 G,连接数为 225 K。

我们简单进行换算,可以看出平均一个链接占用的内存分别为:堆:23K,栈:32K。通过对比业内长连接服务的数据可以看出单个链接占用的内存偏大,根据监控数据和内存分配原理分析主要原因在:goroutine 占用、session 状态信息、pubsub 模块占用,我们打算从业务、程序、网络模式三个方面进行优化。

业务优化

上文中提到 session 模块主要是用于处理消息的收发,在实现时考虑到在通常场景中业务的消息生产大于消息的消费速度的情况,为了缓解这种状况,设计时引入消息的缓冲队列,这种做法同样也有助于做消息的流控。

缓冲消息队列借助chan 实现 ,chan 大小根据经验将初始化默认配置为 128 。但在目前线上推送的场景中,我们发现,消息的生产一般小于消费的速度,128 缓冲大小明显偏大,因此我们把长度调整为 16 ,减少内存的分配。

在设计中按照topic 对进行分组管理的算法中,采用空间换时间的方式,组合 map 和 list 两种数据结构对于集合操作提供O(1)的删除、O(1)的添加、O(n)的遍历。数据的删除采用标记删除方式,使用辅助 slice 结构进行记录,只有到达预设阈值才会进行真正的删除。虽然标记删除提高了遍历和添加的性能,但也同样带来了内存损耗问题。

大家一定好奇什么样的场景需要提供这样的复杂度,在实际中其场景有以下两种情况:

通过benchamrk 数据分析,在标记回收 slice 长度在 1000 时,可以提供最佳的性能,因此默认配置阈值为 1000。在线上服务中,无特殊情况都是采用默认配置。但在当前推送服务的使用中,发现标记删除和延迟回收机制好处甚微,主要是因为 topic 和为 1 : 1 方式,也就是不存在集合,因此调整回收阈值大小为 2,减少无效内存占用。

上述所有优化,只要简单调整配置后服务灰度上线即可,在设计实现时通过conf 模块动态配置,降低了服务的开发和维护成本。通过监控对比优化效果如下表,在优化后在线连接数比优化的在线连接更多的情况下, heap 使用内存使用数量由原来的 4.16G 下降到了 3.5G ,降低了约 0.66 G。

Golang实现单机百万长连接服务 - 美的三年优化经验

golang 代码优化

在实现上面展示的架构的时候发现在session 模块 和 mqttsrv 模块之间存在很多共享变量,目前实现方式都是采用指针或者值拷贝的,由于 session的数量和数据量成正比也就导致消耗大量内存用于共享数据,这不仅仅增加 GC 压力,同样对于内存的消耗也是巨大的。就此问题思考再三,参考系统的库 context 的设计在架构中也抽象 context 包负责模块之间交互信息传递,统一分配内存。此外还参考他人减少临时变量的分配的优化方式,提高系统运行效率。主要优化角度参考如下:

在频繁申请内存的地方,使用pool 方式进行内存管理

小对象合并成结构体一次分配,减少内存分配次数

缓存区内容一次分配足够大小空间,并适当复用

slice 和 map 采 make 创建时,预估大小指定容量

调用栈避免申请较多的临时对象

减少byte 与 string 之间转换,尽量采用 byte 来字符串处理

目前系统具被完备的单元测试、集成测试,因此经过一周的快速的开发重构后灰度上线监控数据对比如下表:在基本相同的连接数上,heap 使用内存约占用降低 0.27G,stack 申请内存占用降低 3.81G。为什么 stack 会大幅度降低呢?

通过设置stackDebug 重新编译程序追查程序运行过程,优化前 goroutine 栈的大多数在内存为 16K,通过减少临时变量的分配,拆分大函数处理逻辑,有效的减少触发栈的内存扩容,优化后 goroutine 栈内存降低到 8 K。一个连接需要启动两个 goroutine 负责数据的读和写,粗略计算一个连接减少约 16 K 的内存,23 w 连接约降低 3.68 G 内存。

Golang实现单机百万长连接服务 - 美的三年优化经验

网络模型优化

在Go 语言的网络编程中经典的实现都是采用同步处理方式,启动两个 goroutine 分别处理读和写请求,goroutine 也不像 thread ,它是轻量级的。但对于一百万连接的情况,这种设计模式至少要启动两百万的 goroutine,其中一个 goroutine 使用栈的大小在 2 KB 到 8KB, 对于资源的消耗也是极大的。在大多数场景中,只有少数连接是有数据处理,大部分 goroutine 阻塞 IO 处理中。在因此可以借鉴 C 语言的设计,在程序中使用 epoll 模型做事件分发,只有活跃连接才会启动 goroutine 处理业务,基于这种思想修改网络处理流程。

网络模型修改测试完成后开始灰度上线,通过监控数据对比如下表:在优化后比优化前的连接数多10 K的情况下,heap 使用内存降低 0.33 G,stack 申请内存降低 2.34 G,优化效果显著。

Golang实现单机百万长连接服务 - 美的三年优化经验

在经过业务优化,临时内存优化,网络模型优化操作后,线上服务保证21w 长连接在线实际内存占用约为 5.1 G。简单进行压测 100w 连接只完成建立连接,不进行其他操作约占用 10 G。长连接服务内存优化已经取得阶段性的成功,但是这仅仅是我们团队的一小步,未来还有更多的工作要做:网络链路、服务能力,存储优化等,这些都是亟待探索的方向。如果大家有什么好的想法,欢迎与我们团队分享,共同探讨。

bifrost项目目前我们有开源计划,敬请大家期待。

参考文章

go tool pprof 使用介绍 :https://segmentfault.com/a/1190000016412013

Go 内存监控介绍:https://golang.org/src/runtime/mstats.go

Go 内存优化介绍:https://blog.golang.org/profiling-go-programs

高性能Go服务内存分配:https://segment.com/blog/allocation-efficiency-in-high-performance-go-services

Go stack 优化分析:https://studygolang.com/articles/10597

参考阅读:

正式支持多线程!Redis 6.0与老版性能对比评测

你真的了解性能压测中的SLA吗?

一个Netflix开发的微服务编排引擎,支持可视化工作流定义

你真的了解压测吗?实战讲述性能测试场景设计和实现

关于Golang GC的一些误解--真的比Java算法更领先吗?

高可用架构

改变互联网的构建方式

Go 官宣:新版 Protobuf API

原文链接:https://blog.golang.org/a-new-go-api-for-protocol-buffers

翻译:polaris,Go 语言中文网

Go 官宣:新版 Protobuf API

简介

我们很高兴地宣布,用于 Google 的语言无关数据交换格式 protocol buffers[1] 的新版 Go API 了,这是一次重大的版本升级。

新 API 的动机

Go 的第一个 protocol buffer 绑定由 Rob Pike 于 2010 年 3 月宣布[2]。两年后 Go1 才。

自从首次以来的十年中,该包与 Go 一起发展壮大。它的用户需求也有所增长。

许多人想编写使用反射来检查 protocol buffer 的程序。反射包[3]提供了 Go 类型和值的视,但是忽略了 protocol buffer 类型系统中的信息。例如,我们可能想编写一个遍历日志条目并清除任何注解(annotation)为包含敏感数据的字段的函数。注解不是 Go 类型系统的一部分。

另一个常见的需求是使用 protocol buffer 编译器生成的数据结构之外的其他数据结构,例如能够代表其消息类型在编译时未知的动态消息类型。

我们还观察到,常见的问题根源是 proto.Message[4] 接口,它标识生成的消息类型的值,对描述这些类型的行为几乎没有作用。当用户创建实现该接口的类型(通常通过在另一个结构中嵌入消息而无意间实现)并将这些类型的值传递给期望生成消息值的函数时,程序崩溃或行为异常。

以上这三个问题都有一个共同的原因,并且有一个共同的解决方案:Message 接口应完全指定消息的行为,并且对 Message 值进行操作的函数应自由接受可以正确实现该接口的任何类型。

由于在保持包 API 兼容的同时无法更改 Message 类型的现有定义,因此我们决定是时候开始研究 protobuf 模块的新的,不兼容的主要版本了。

今天,我们很高兴该新模块。我们希望您能喜欢。

反射

反射是新实现的旗舰特性。类似于 reflect 包如何提供 Go 类型和值的视, google.golang.org/protobuf/reflect/protoreflect[5] 包提供了根据 protocol buffer 类型系统的值视。

对 protoreflect 包的完整描述对于这篇文章来说可能会花很长时间,但是让我们看一下如何编写我们前面提到的 log-scrubbing 函数。

首先,我们将编写一个 .proto 文件,该文件定义 google.protobuf.FieldOptions[6] 类型的扩展名,以便我们可以将字段注解为包含或不包含敏感信息。

syntax = "proto3";import "google/protobuf/descriptor.proto";package golang.example.policy;extend google.protobuf.FieldOptions {    bool non_sensitive = 50000;}

我们可以使用此选项将某些字段标记为不敏感。

message MyMessage {    string public_name = 1 [(golang.example.policy.non_sensitive) = true];}

接下来,我们将编写一个 Go 函数,该函数接受任意消息值并删除所有敏感字段。

// Redact clears every sensitive field in pb.func Redact(pb proto.Message) {   // ...}

此函数接受 proto.Message[7],这是由所有生成的消息类型实现的接口类型。此类型是 protoreflect 包中定义的别名:

type ProtoMessage interface{    ProtoReflect() Message}

为了避免填充所生成消息的名称空间,该接口仅包含一个返回 protoreflect.Message[8]的方法,该方法提供对消息内容的访问。

(为什么要使用别名?因为 protoreflect.Message 具有返回原始 proto.Message 的相应方法,所以我们需要避免两个包之间的循环导入。)

protoreflect.Message.Range[9] 方法为消息中的每个填充字段调用一个函数。

m := pb.ProtoReflect()m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {    // ...    return true})

使用描述字段的 protocol buffer 类型的 protoreflect.FieldDescriptor[10] 和包含字段值的 protoreflect.Value[11] 来调用 range 函数。

protoreflect.FieldDescriptor.Options[12] 方法以 google.protobuf.FieldOptions 消息的形式返回字段选项。

opts := fd.Options().(*descriptorpb.FieldOptions)

(为什么要使用类型断言?由于生成的描述符 pb 包依赖于 protoreflect,因此 protoreflect 包不能在不引起循环导入的情况下返回具体的选项类型。)

然后,我们可以检查选项以查看扩展布尔值:

if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {    return true // don't redact non-sensitive fields}

请注意,我们在这里查看的是字段描述符,而不是字段值。我们感兴趣的信息在于 protocol buffer 类型系统,而不是 Go 语言。

这也是我们简化了 proto 包 API 的一个示例。原来的 proto.GetExtension[13] 返回一个值和一个错误。新的proto.GetExtension[14] 仅返回一个值,如果不存在该字段,则返回默认值。在 Unmarshal 时间时报告扩展解码错误。

一旦我们确定了需要修改的字段,将其清除很简单:

m.Clear(fd)

综上所述,我们完整的修改功能是:

// Redact clears every sensitive field in pb.func Redact(pb proto.Message) {    m := pb.ProtoReflect()    m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {        opts := fd.Options().(*descriptorpb.FieldOptions)        if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {            return true        }        m.Clear(fd)        return true    })}

更完整的实现可以递归地深入消息值字段。我们希望这个简单的例子能使您了解 protocol buffer 反射及其用法。

版本号

我们将 Go protocol buffers 的原始版本称为 APIv1,将新版本称为 APIv2。由于 APIv2 与 APIv1 不向后兼容,因此我们需要为每个 APIv 使用不同的模块路径。

(这些 API 版本与 protocol buffers 语言的版本不同:proto1,proto2 和 proto3。APIv1 和 APIv2 是 Go 中的具体实现,均支持 proto2 和 proto3 语言版本。)

github.com/golang/protobuf[15] 是模块 APIv1 版本。

google.golang.org/protobuf[16] 模块是 APIv2。我们利用了需要更改导入路径以切换到与特定托管服务提供商无关的路径的优势。(我们考虑了 google.golang.org/protobuf/v2,以明确说明这是该 API 的第二个主要版本,但从长远来看,选择较短的路径是更好的选择。)

我们知道,并非所有用户都将以相同的速度迁移到包的新主版本。有些会快速切换;其他可能会无限期保留在旧版本中。即使在一个程序中,某些部分可能使用一个 API,而其他部分则使用另一个。因此,我们必须继续支持使用 APIv1 的程序。

github.com/golang/protobuf@v1.3.4 是 APIv1 最新 pre-APIv2 版本。

github.com/golang/protobuf@v1.4.0 是根据 APIv2 实现的 APIv1 版本。它的 API 相同,但是基础实现由新的实现支持。此版本包含在 APIv1 和 APIv2 proto.Message 接口之间转换的函数,以简化两者之间的过渡。

google.golang.org/protobuf@v1.20.0 是 APIv2。该模块依赖 github.com/golang/protobuf@v1.4.0,因此任何使用 APIv2 的程序都会自动选择与其集成的 APIv1 版本。

(为什么要从 v1.20.0 版本开始?为了清楚起见。我们认为 APIv1 不会达到 v1.20.0,因此仅版本号就应该足以区分 APIv1 和 APIv2。)

我们打算无限期地保持对 APIv1 的支持。

该组织确保任何给定程序都将仅使用单个 protocol buffer 实现,而不管其使用哪个 API 版本。它允许程序逐渐或根本不采用新的 API,同时仍获得新实现的优点。最小版本选择的原则意味着程序可以保留在旧的实现上,直到维护者选择更新到新的(直接或通过更新依赖项)。

值得的其他特性

google.golang.org/protobuf/encoding/protojson[17] 包使用规范的 JSON 映射将 protocol buffer 消息与 JSON 相互转换,并解决了旧 jsonpb 包难以解决的许多问题,而这些问题不会对现有用户造成问题。

google.golang.org/protobuf/types/dynamicpb[18] 包为 protocol buffer 类型在运行时派生的消息提供了 proto.Message 的实现。

google.golang.org/protobuf/testing/protocmp[19] 包提供了将 protocol buffer 消息与 github.com/google/cmp[20] 包进行比较的功能。

google.golang.org/protobuf/compiler/protogen[21] 包提供了对编写协议编译器插件的支持。

总结

google.golang.org/protobuf 模块是 Go 对 protocol buffers 支持的主要改进,它为反射,自定义消息实现和清理的 API surface 提供了优先支持。我们打算无限期地维护以前的 API 作为新 API 的包装,从而使用户可以按照自己的步调逐步采用新 API。

我们在此更新的目标是在解决旧 API 的缺点的同时,提升旧 API 的优势。完成新实现的每个组件后,我们将其在 Google 的代码库中投入使用。这种逐步推出的方式使我们对新 API 的可用性以及新实现的性能和正确性都充满信心。我们相信已经准备好用于生产环境了。

我们很兴奋地了该版本,并希望它将在未来十年甚至更长时间内为 Go 生态系统服务!

参考资料

[1]

protocol buffers: https://developers.google.com/protocol-buffers

[2]

Rob Pike 于 2010 年 3 月宣布: https://blog.golang.org/third-party-libraries-goprotobuf-and

[3]

反射包: https://docs.studygolang.com/pkg/reflect

[4]

proto.Message: https://pkg.go.dev/github.com/golang/protobuf/proto?tab=doc#Message

[5]

google.golang.org/protobuf/reflect/protoreflect: https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc

[6]

google.protobuf.FieldOptions: https://github.com/protocolbuffers/protobuf/blob/b96241b1b716781f5bc4dc25e1ebb0003dfaba6a/src/google/protobuf/descriptor.proto#L509

[7]

proto.Message: https://pkg.go.dev/google.golang.org/protobuf/proto?tab=doc#Message

[8]

protoreflect.Message: https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Message

[9]

protoreflect.Message.Range: https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Message.Range

[10]

protoreflect.FieldDescriptor: https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#FieldDescriptor

[11]

protoreflect.Value: https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Value

[12]

protoreflect.FieldDescriptor.Options: https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Descriptor.Options

[13]

proto.GetExtension: https://pkg.go.dev/google.golang.org/protobuf/proto?tab=doc#GetExtension

[14]

proto.GetExtension: https://pkg.go.dev/google.golang.org/protobuf/proto?tab=doc#GetExtension

[15]

github.com/golang/protobuf: https://pkg.go.dev/github.com/golang/protobuf?tab=overview

[16]

google.golang.org/protobuf: https://pkg.go.dev/google.golang.org/protobuf?tab=overview

[17]

google.golang.org/protobuf/encoding/protojson: https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson

[18]

google.golang.org/protobuf/types/dynamicpb: https://pkg.go.dev/google.golang.org/protobuf/types/dynamicpb

[19]

google.golang.org/protobuf/testing/protocmp: https://pkg.go.dev/google.golang.org/protobuf/testing/protocmp

[20]

github.com/google/cmp: https://pkg.go.dev/github.com/google/go-cmp/cmp

[21]

google.golang.org/protobuf/compiler/protogen: https://pkg.go.dev/google.golang.org/protobuf/compiler/protogen?tab=doc