Skip to content

Commit 19cad68

Browse files
authored
Create index.md (#1183)
1 parent a96475b commit 19cad68

File tree

1 file changed

+89
-0
lines changed
  • content/zh/blog/moe-series (5)|envoy-go-extensions-memory-security

1 file changed

+89
-0
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
---
2+
title: "MoE 系列(五)|Envoy Go 扩展之内存安全"
3+
authorlink: "https://github.com/sofastack"
4+
description: "MoE 系列(五)|Envoy Go 扩展之内存安全"
5+
categories: "SOFAStack"
6+
tags: ["SOFAStack"]
7+
date: 2023-05-30T15:00:00+08:00
8+
cover: "https://mdn.alipayobjects.com/huamei_soxoym/afts/img/A*0kzhRbFN4HcAAAAAAAAAAAAADrGAAQ/original"
9+
10+
---
11+
12+
**前面几篇介绍了 Envoy Go 扩展的基本用法,接下来几篇将介绍实现机制和原理。**
13+
14+
Envoy 是 C++ 实现的,那 Envoy Go 扩展,本质上就相当于把 Go 语言嵌入 C++ 里了。
15+
16+
在 Go 圈里,将 Go 当做嵌入式语言来用的,貌似并不太多见,这里面细节还是比较多的。比如:
17+
18+
1. Envoy 有一套自己的内存管理机制,而 Go 又是一门自带 GC 的语言。
19+
2. Envoy 是基于 libevent 封装的事件驱动,而 Go 又是包含了抢占式的协程调度。
20+
21+
为了降低用户开发时的心智负担,我们提供了三种安全保障。有了这三层保障,用户写 Go 来扩展 Envoy 的时候,就可以像平常写 Go 代码一样简单,而不必关心这些底层细节。
22+
23+
## 三种安全
24+
25+
### 1. 内存安全
26+
27+
用户通过 API 获取到的内存对象,可以当做普通的 Go 对象来使用。
28+
29+
比如,通过 Headers.Get 得到的字符串,在请求结束之后还可以使用,而不用担心请求已经在 Envoy 侧结束了,导致这个字符串被提前释放了。
30+
31+
### 2. 并发安全
32+
33+
当启用协程的时候,我们的 Go 代码将会运行在另外的 Go 线程上,而不是在当前的 Envoy worker 线程上,此时对于同一个请求,则存在 Envoy worker 线程和 Go 线程的并发。
34+
35+
但是,用户并不需要关心这个细节,我们提供的 API 都是并发安全的,用户可以不感知并发的存在。
36+
37+
### 3. 沙箱安全
38+
39+
这一条是针对宿主 Envoy 的保障,因为我们并不希望某一个 Go 扩展的异常,把整个 Envoy 进程搞崩溃。
40+
41+
目前我们提供的是,Go Runtime 可以 recover 的有限沙箱安全,这通常也足够了。
42+
43+
更深度的,Runtime 不能 recover 的,比如 Map 并发访问,则只能将 Go So 重载,重建整个 Go Runtime 了,这个后续也可以加上。
44+
45+
## 内存安全实现机制
46+
47+
要提供安全的内存机制,最简单的办法,也是*(几乎)*唯一的办法,就是复制。但是,什么时候复制、怎么复制,还是有一些讲究的。这里权衡的目标是降低复制的开销,提升性能。
48+
49+
这里讲的内存安全,还不涉及并发时的内存安全,只是 Envoy*(C++)*和 Go 这两个语言运行时之间的差异。
50+
51+
PS:以前用 OpenResty 的时候,也是复制的玩法,只是有一点区别是,Lua String 的 Internal 归一化在大内存场景下,会有相对较大的开销;Go String 则没有这一层开销,只有 Memory Copy + GC 的开销。
52+
53+
### 复制时机
54+
55+
首先是复制时机,我们选择了按需复制,比如 Header,Body Data 并不是一开始就复制到 Go 里面,只有在对应的 API 调用时,才会真的去 Envoy 侧获取&复制。
56+
57+
如果没有被真实需要,则并不会产生复制,这个优化对于 Header 这种常用的,效果倒是不太明显,对于 Body 这种经常不需要获取内容的,效果则会比较的明显。
58+
59+
### 复制方式
60+
61+
另一个则是复制方式,比如 Header 获取上,我们采用的是在 Go 侧预先申请内存,在 C++ 侧完成赋值的方式,这样我们只需要一次内存赋值即可完成。
62+
63+
这里值得一提的是,因为我们在进入 Go 的时候,已经把 Header 的大小传给了 Go,所以我们可以在 Go 侧预先分配好需要的内存。
64+
65+
不过呢,这个玩法确实有点 tricky,并不是 Go 文档上注明推荐的用法,但是也确实是我们发现的最优的解法了。
66+
67+
如果按照 Go 常规的玩法,我们可能需要一次半或两次内存拷贝,才能保证安全,这里有个半次的差异,就是我们下回要说的并发造成的。
68+
69+
另外,在 API 实现上,我们并不是每次获取一个 Header,而是直接一次性把所有的 Header 全复制过来,在 Go 侧缓存了。这是因为大多数场景下,我们需要获取的 Header 数量会有多个,在权衡了 CGO 的调用开销和内存拷贝的开销之后,我们认为一次性全拷贝是更优的选择。
70+
71+
## 最后
72+
73+
相对来说,不考虑并发的内存安全,还是比较简单的,只有复制最安全,需要权衡考虑的则更多是优化的事情了。
74+
75+
比较复杂的还是并发时的安全处理,这个我们下回再聊。
76+
77+
## MOSN Star 一下✨:
78+
79+
[https://github.com/mosn/mosn](https://github.com/mosn/mosn)
80+
81+
## 推荐阅读
82+
83+
[MoE 系列(一)|如何使用 Golang 扩展 Envoy](https://mp.weixin.qq.com/s/GF5Pr2aAOe6NAdJ5VgfMvg)
84+
85+
[MoE 系列(二)|Golang 扩展从 Envoy 接收配置](https://mp.weixin.qq.com/s/xRt9qet-Dm3UMEVa3iDFrA)
86+
87+
[MoE 系列(三)|使用 Istio 动态更新 Go 扩展配置](https://mp.weixin.qq.com/s/gvbvAZEUbjtD-UpKziHmBA)
88+
89+
[MoE 系列(四)|Go 扩展的异步模式](

0 commit comments

Comments
 (0)