后面几篇介绍了 Envoy Go 扩大的根本用法,接下来几篇将介绍实现机制和原理。

Envoy 是 C++ 实现的,那 Envoy Go 扩大,实质上就相当于把 Go 语言嵌入 C++ 里了。

在 Go 圈里,将 Go 当做嵌入式语言来用的,貌似并不太多见,这外面细节还是比拟多的。比方:

  1. Envoy 有一套本人的内存管理机制,而 Go 又是一门自带 GC 的语言。
  2. Envoy 是基于 libevent 封装的事件驱动,而 Go 又是蕴含了抢占式的协程调度。

为了升高用户开发时的心智累赘,咱们提供了三种平安保障。有了这三层保障,用户写 Go 来扩大 Envoy 的时候,就能够像平时写 Go 代码一样简略,而不用关怀这些底层细节。

三种平安

1. 内存平安

用户通过 API 获取到的内存对象,能够当做一般的 Go 对象来应用。

比方,通过 Headers.Get 失去的字符串,在申请完结之后还能够应用,而不必放心申请曾经在 Envoy 侧完结了,导致这个字符串被提前开释了。

2. 并发平安

当启用协程的时候,咱们的 Go 代码将会运行在另外的 Go 线程上,而不是在以后的 Envoy worker 线程上,此时对于同一个申请,则存在 Envoy worker 线程和 Go 线程的并发。

然而,用户并不需要关怀这个细节,咱们提供的 API 都是并发平安的,用户能够不感知并发的存在。

3. 沙箱平安

这一条是针对宿主 Envoy 的保障,因为咱们并不心愿某一个 Go 扩大的异样,把整个 Envoy 过程搞解体。

目前咱们提供的是,Go Runtime 能够 recover 的无限沙箱平安,这通常也足够了。

更深度的,Runtime 不能 recover 的,比方 Map 并发拜访,则只能将 Go So 重载,重建整个 Go Runtime 了,这个后续也能够加上。

内存平安实现机制

要提供平安的内存机制,最简略的方法,也是 (简直) 惟一的方法,就是复制。然而,什么时候复制、怎么复制,还是有一些考究的。这里衡量的指标是升高复制的开销,晋升性能。

这里讲的内存平安,还不波及并发时的内存平安,只是 Envoy (C++) 和 Go 这两个语言运行时之间的差别。

PS:以前用 OpenResty 的时候,也是复制的玩法,只是有一点区别是,Lua String 的 Internal 归一化在大内存场景下,会有绝对较大的开销;Go String 则没有这一层开销,只有 Memory Copy + GC 的开销。

复制机会

首先是复制机会,咱们抉择了按需复制,比方 Header,Body Data 并不是一开始就复制到 Go 外面,只有在对应的 API 调用时,才会真的去 Envoy 侧获取&复制。

如果没有被实在须要,则并不会产生复制,这个优化对于 Header 这种罕用的,成果倒是不太显著,对于 Body 这种常常不须要获取内容的,成果则会比拟的显著。

复制形式

另一个则是复制形式,比方 Header 获取上,咱们采纳的是在 Go 侧事后申请内存,在 C++ 侧实现赋值的形式,这样咱们只须要一次内存赋值即可实现。

这里值得一提的是,因为咱们在进入 Go 的时候,曾经把 Header 的大小传给了 Go,所以咱们能够在 Go 侧事后调配好须要的内存。

不过呢,这个玩法的确有点 tricky,并不是 Go 文档上注明举荐的用法,然而也的确是咱们发现的最优的解法了。

如果依照 Go 惯例的玩法,咱们可能须要一次半或两次内存拷贝,能力保障平安,这里有个半次的差别,就是咱们下回要说的并发造成的。

另外,在 API 实现上,咱们并不是每次获取一个 Header,而是间接一次性把所有的 Header 全复制过去,在 Go 侧缓存了。这是因为大多数场景下,咱们须要获取的 Header 数量会有多个,在衡量了 CGO 的调用开销和内存拷贝的开销之后,咱们认为一次性全拷贝是更优的抉择。

最初

相对来说,不思考并发的内存平安,还是比较简单的,只有复制最平安,须要衡量思考的则更多是优化的事件了。

比较复杂的还是并发时的平安解决,这个咱们下回再聊。

MOSN Star 一下✨:

https://github.com/mosn/mosn

举荐浏览

MoE 系列(一)|如何应用 Golang 扩大 Envoy

MoE 系列(二)|Golang 扩大从 Envoy 接管配置

MoE 系列(三)|应用 Istio 动静更新 Go 扩大配置

MoE 系列(四)|Go 扩大的异步模式