MoE 系列(五)|Envoy Go 扩展之内存安全

简介: 前面几篇介绍了 Envoy Go 扩展的基本用法,接下来几篇将介绍实现机制和原理。

前面几篇介绍了 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 扩展的异步模式

相关文章
|
1月前
|
编译器 Go
探索 Go 语言中的内存对齐:为什么结构体大小会有所不同?
在 Go 语言中,内存对齐是优化内存访问速度的重要概念。通过调整数据在内存中的位置,编译器确保不同类型的数据能够高效访问。本文通过示例代码展示了两个结构体 `A` 和 `B`,尽管字段相同但排列不同,导致内存占用分别为 40 字节和 48 字节。通过分析内存布局,解释了内存对齐的原因,并提供了优化结构体字段顺序的方法,以减少内存填充,提高性能。
42 3
|
1月前
|
Java 编译器 测试技术
go语言避免不必要的内存分配
【10月更文挑战第18天】
50 1
|
1月前
|
存储 算法 Java
Go语言的内存管理机制
【10月更文挑战第25天】Go语言的内存管理机制
35 2
|
4月前
|
存储 安全 编译器
Go 内存分布
该文章深入分析了Go语言中值的内存分布方式,特别是那些分布在多个内存块上的类型,如切片、映射、通道、函数、接口和字符串,并讨论了这些类型的内部结构和赋值时的行为,同时指出了“引用类型”这一术语在Go中的使用可能会引起的误解。
59 5
Go 内存分布
|
2月前
|
SQL 存储 Java
关于内存安全问题,你应该了解的几点!
关于内存安全问题,你应该了解的几点!
|
3月前
|
安全 Java API
【性能与安全的双重飞跃】JDK 22外部函数与内存API:JNI的继任者,引领Java新潮流!
【9月更文挑战第7天】JDK 22外部函数与内存API的发布,标志着Java在性能与安全性方面实现了双重飞跃。作为JNI的继任者,这一新特性不仅简化了Java与本地代码的交互过程,还提升了程序的性能和安全性。我们有理由相信,在外部函数与内存API的引领下,Java将开启一个全新的编程时代,为开发者们带来更加高效、更加安全的编程体验。让我们共同期待Java在未来的辉煌成就!
77 11
|
3月前
|
安全 Java API
【本地与Java无缝对接】JDK 22外部函数和内存API:JNI终结者,性能与安全双提升!
【9月更文挑战第6天】JDK 22的外部函数和内存API无疑是Java编程语言发展史上的一个重要里程碑。它不仅解决了JNI的诸多局限和挑战,还为Java与本地代码的互操作提供了更加高效、安全和简洁的解决方案。随着FFM API的逐渐成熟和完善,我们有理由相信,Java将在更多领域展现出其强大的生命力和竞争力。让我们共同期待Java编程新纪元的到来!
123 11
|
4月前
|
数据采集 Rust 安全
Rust在网络爬虫中的应用与实践:探索内存安全与并发处理的奥秘
【8月更文挑战第31天】网络爬虫是自动化程序,用于从互联网抓取数据。随着互联网的发展,构建高效、安全的爬虫成为热点。Rust语言凭借内存安全和高性能特点,在此领域展现出巨大潜力。本文探讨Rust如何通过所有权、借用及生命周期机制保障内存安全;利用`async/await`模型和`tokio`运行时处理并发请求;借助WebAssembly技术处理动态内容;并使用`reqwest`和`js-sys`库解析CSS和JavaScript,确保代码的安全性和可维护性。未来,Rust将在网络爬虫领域扮演更重要角色。
91 1
|
4月前
|
存储 安全 Go
Go 中的指针:了解内存引用
Go 中的指针:了解内存引用
|
4月前
|
缓存 Java 编译器
Go 中的内存布局和分配原理
Go 中的内存布局和分配原理