0%

前两篇介绍了内存安全和并发安全,今天来到了安全性的最后一篇,沙箱安全,也是相对来说,最简单的一篇。

沙箱安全

所谓的沙箱安全,是为了保护 Envoy,这个宿主程序的安全,也就是说,扩展的 Go 代码运行在一个沙箱环境中,即使 Go 代码跑飞了,也不会把 Envoy 搞挂。

具体到一个场景,也就是当我们使用 Golang 来扩展 Envoy 的时候,不用担心自己的 Go 代码写的不好,而把整个 Envoy 进程搞挂了。

那么目前 Envoy Go 扩展的沙箱安全做到了什么程度呢?

简单来说,目前只做到了比较浅层次的沙箱安全,不过,也是实用性比较高的一层。

严格来说,Envoy Go 扩展加载的是可执行的机器指令,是直接交给 cpu 来运行的,并不像 Wasm 或者 Lua 一样由虚拟机来解释执行,所以,理论上来说,也没办法做到绝对的沙箱安全。

实现机制

目前实现的沙箱安全机制,依赖的是 Go runtime 的 recover 机制。

具体来说,Go 扩展底层框架会自动的,或者(代码里显示启动的协程)依赖人工显示的,通过 defer 注入我们的恢复机制,所以,当 Go 代码发生了奔溃的时候,则会执行我们注入的恢复策略,此时的处理策略是,使用 500 错误码结束当前请求,而不会影响其他请求的执行。

但是这里有一个不太完美的点,有一些异常是 recover 也不能恢复的,比如这几个:

1
2
3
4
5
Concurrent map writes
Out of memory
Stack memory exhaustion
Attempting to launch a nil function as a goroutine
All goroutines are asleep - deadlock

好在这几个异常,都是不太容易出现的,唯一一个值得担心的是 Concurrent map writes,不熟悉 Go 的话,还是比较容易踩这个坑的。

所以,在写 Go 扩展的时候,我们建议还是小心一些,写得不好的话,还是有可能会把 Envoy 搞挂的。

当然,这个也不是一个很高的要求,毕竟这是 Gopher 写 Go 代码的很常见的基本要求。

好在大多常见的异常,都是可以 recover 恢复的,这也就是为什么现在的机制,还是比较有实用性。

未来

那么,对于 recover 恢复不了的,也是有解决的思路:

比如 recover 恢复不了 Concurrent map writes,是因为 runtime 认为 map 已经被写坏了,不可逆了。

那如果我们放弃整个 runtime,重新加载 so 来重建 runtime 呢?那影响面也会小很多,至少 Envoy 还是安全的,不过实现起来还是比较的麻烦。

眼下比较浅的安全机制,也足够解决大多数的问题了,嗯。

前一篇介绍了 Envoy Go 扩展的内存安全,相对来说,还是比较好理解的,主要是 Envoy C++ 和 Go GC 都有自己一套的内存对象的生命周期管理。

这篇聊的并发安全,则是专注在并发场景下的内存安全,相对来说会复杂一些。

并发的原因

首先,为什么会有并发呢?

本质上因为 Go 有自己的抢占式的协程调度,这是 Go 比较重的部分,也是与 Lua 这类嵌入式语言区别很大的点。

细节的话,这里就不展开了,感兴趣的可以看这篇 cgo 实现机制 - 从 c 调用 go

这里简单交代一下的,因为 c 调用 go,入口的 Go 函数的运行环境是,Goroutine 运行在 Envoy worker 线程上,但是这个时候,如果发生了网络调用这种可能导致 Goroutine 挂起的,则会导致 Envoy worker 线程被挂起。

所以,解决思路就是像 Go 扩展的异步模式 中的示例一样,新起一个 Goroutine,它会运行在普通的 go 线程上。

那么此时,对于同一个请求,则会同时有 Envoy worker 线程和 Go 线程,两个线程并发在处理这个请求,这个就是并发的来源。

但是,我们并不希望用户操心这些细节,而是在底层提供并发安全的 API,把复杂度留在 Envoy Go 扩展的底层实现里。

并发安全的实现

接下来,我们就针对 Goroutine 运行在普通的 Go 线程上,这个并发场景,来聊一聊如何实现并发安全的。

对于 Goroutine 运行在 Envoy 线程上,因为并不存在并发冲突,这里不做介绍。

写 header 操作

我们先聊一个简单的,比如在 Go 里面通过 header.Set 写一个请求头。

核心思路是,是通过 dispatcher.post,将写操作当做一个事件派发给 Envoy worker 线程来执行,这样就避免了并发冲突。

读 header 操作

读 header 则要复杂不少,因为写不需要返回值,可以异步执行,读就不行了,必须得到返回值。

为此,我们根据 Envoy 流式的处理套路,设计了一个类似于所有权的机制。

Envoy 的流式处理,可以看这篇 搞懂 http filter 状态码

简单来说,我们可以这么理解,当进入 decodeHeaders 的时候,header 所有权就交给 Envoy Go 的 c++ 侧了,然后,当通过 cgo 进入 Go 之后,我们会通过一个简单的状态机,标记所有权在 Go 了。

通过这套设计/约定,就可以安全的读取 header 了,本质上,还是属于规避并发冲突。

为什么不通过锁来解决呢?因为 Envoy 并没有对于 header 的锁机制,c++ 侧完全不会有并发冲突。

读写 data 操作

有了这套所有权机制,data 操作就要简单很多了。

因为 header 只有一份,并发冲突域很大,需要考虑 Go 代码与 c++ 侧的其他 filter 的竞争。

data 则是流式处理,我们在 c++ 侧设计了两个 buffer 对象,一个用于接受 filter manager 的流式数据,一个用于缓存交给 Go 侧的数据。

这样的话,交给 Go 来处理的数据,Go 代码拥有完整的所有权,不需要考虑 Go 代码与 C++ 侧其他 filter 的竞争,可以安全的读写,也没有并发冲突。

请求生命周期

另外一个很大的并发冲突,则关乎请求的生命周期,比如 Envoy 随时都有可能提前销毁请求,此时 Goroutine 还在 go thread 上继续执行,并且随时可能读写请求数据。

处理的思路是:

  1. 并没有有效的办法,能够立即 kill goroutine,所以,我们允许 goroutine 可能在请求被销毁之后继续执行
  2. 但是,goroutine 如果读写请求数据,goroutine 会被终止,panic + recover,具体我们下一篇再介绍。

那么,我们要做的就是,所有的 API 都检查当前操作的请求是否合法,这里有两个关键:

  1. 每请求有一个内存对象,这个对象只会由 Go 来销毁,并不会在请求结束时,被 Envoy 销毁,但是这个内存对象中保存了一个 weakPtr,可以获取 C++ filter 的状态。

    通过这个机制,Go 可以安全的获取 C++ 侧的 filter,判断请求是否还在。

  2. 同时,我们还会在 onDestroy,也就是 C++ filter 被销毁的 hook 点;以及 Go thread 读写请求数据,这两个位置都加锁处理,以解决这两个之间的并发冲突。

最后

对于并发冲突,其实最简单的就是,通过加锁来竞争所有权,但是 Envoy 在这块的底层设计并没有锁,因为它根本不需要锁。

所以,基于 Envoy 的处理模型,我们设计了一套类似所有权的机制,来避免并发冲突。

所有权的概念也受到了 Rust 的启发,只是两者工作的层次不一样,Rust 是更底层的语言层面,可以作用于语言层面,我们这里则是更上层的概念,特定于 Envoy 的处理模型,也只能作用于这一个小场景。

但是某种程度上,解决的问题,以及其中部分思想是一样的。

前面几篇介绍了 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 的调用开销和内存拷贝的开销之后,我们认为一次性全拷贝是更优的选择。

最后

相对来说,不考虑并发的内存安全,还是比较简单的,只有复制最安全,需要权衡考虑的则更多是优化的事情了。

比较复杂的还是并发时的安全处理,这个我们下回再聊。

cgo 优化被合并主干之后,很高兴的写了篇流水账,倾诉了一番心酸史,也感谢各位大佬的转发,收获了写公众号以来最多的围观

然而打脸来得也真快,就在昨儿,愚人节的早上,被 revert 了 …

好在 Cherry 大佬还有意继续推进,搞了个 CL 481061 ,Michael 大佬也给了 +2 approve,希望能合回去,赶上 1.21 这个版本。

挖坑小能手

我们看看 Cherry 大佬最新这个 CL 的 commit log 中的描述:

1
2
3
4
5
6
7
8
CL 392854, by doujiang24 <doujiang24@gmail.com>, speed up C to Go
calls by binding the M to the C thread. See below for its
description.
CL 479255 is a followup fix for a small bug in ARM assembly code.
CL 479915 is another followup fix to address C to Go calls after
the C code uses some stack, but that CL is also buggy.
CL 481057, by Michael Knyszek, is a followup fix for a memory leak
bug of CL 479915.

最初我搞的 CL 392854 被合并之后,大佬们已经帮忙搞了三个 bugfix 了,真是惭愧…

最开始的 CL 479255,是一个低级 bug,arm 的汇编少复制了一行,没啥好说的

morestack on g0

CL 479915 则是费了一番功夫才搞明白…

Cherry 大佬的描述在这个 issue 59294

g0 是干啥的

要讲清楚这个 bug,得先介绍一下 g0

每个 M 都有一个 g0,来处理一些特殊的事情,比如扩栈的时候,newstack 函数就运行在 g0 上。

如下 morestack 的代码:

1
2
3
4
5
6
7
// Call newstack on m->g0's stack.
MOVQ m_g0(BX), BX
MOVQ BX, g(CX)
MOVQ (g_sched+gobuf_sp)(BX), SP
MOVQ (g_sched+gobuf_bp)(BX), BP
CALL runtime·newstack(SB)
CALL runtime·abort(SB) // crash if newstack returns

这里是先切换到 g0 栈,再运行的 newstack,结合 g 的数据结构,核心就是这行伪代码

1
SP = g0.sched.sp

问题

但是,在 cgo 启用的时候,g0 并不会像普通的 g 一样,拥有自己的 stack 空间,而是会复用 C 线程的 stack 空间。

我们可以看 needmg0 栈的处理,g0 栈就是当前 C 栈顶往下的 32 kb 地址空间

1
2
3
gp.stack.hi = getcallersp() + 1024 // 这个 1024 没搞懂是为啥
gp.stack.lo = getcallersp() - 32*1024
gp.stackguard0 = gp.stack.lo + _StackGuard

优化之前,每次 c 调用 go 都会执行 needm,也就是 g0 的栈会根据当前 c 栈来动态计算;

但是,优化之后,并不是每次 c 调用 go 都会执行 needm 了,也就是 g0 栈固定在第一次进入 go 时计算的栈空间了。

也就是 stackguard0 是固定的了,如果后续 c 调用 go 的时候,c 栈比第一次高很多,这可能就会导致,runtime 在栈检查的时候认为 g0 栈 overflow 了;而 g0 的栈是不能扩的,也就会抛 morestack on g0 的异常。

栈检查

这里稍微解释下 Go 栈检查的逻辑,比如 amd64 上,我们经常会看到这样的指令:

1
2
3
4
5
6
000000000008b100 <runtime.main.func1>:
8b100: 49 3b 66 10 cmp rsp,QWORD PTR [r14+0x10]
8b104: 76 2d jbe 8b133 <runtime.main.func1+0x33>
...
8b133: e8 e8 8a 02 00 call b3c20 <runtime.morestack_noctxt.abi0>
8b138: eb c6 jmp 8b100 <runtime.main.func1>

核心是这样的伪代码:

1
if rsp <= g.stack.stackguard0; { runtime.morestack_noctx() }

也就是拿栈顶 rspstackguard0 对比,小于 stackguard0,则需要扩栈。

对应上面问题的场景,也就是 stackguard0 是固定的,但是 rsp 则是每次不一样的。

解决办法

Cherry 大佬在 issue 里说的解法有两种:

  1. 每次 c 调用的时候,重新为 g0 计算栈空间,这样就跟优化之前保持一致的行为。
  2. g0 的栈顶地址,设置为 c 的栈顶;本质上就是把 g0 的栈空间搞到最大,这样就不容易达到扩栈的条件

大佬在 CL 479915 选择了第二种解法,patch 都搞到 13 个版本才合并,我一路看下来,最大的感觉是,系统兼容性真难搞。

不过,这里还有一个小隐患,也就是栈顶是直接拉满够用了,但是栈底实际上还是不对的,比如 morestack 切栈的时候,会不会切到 c 栈使用了的空间,把 c 栈写坏了呢?

目前看下来,即使在复用 M 的时候,也会有这段逻辑,也就是会将真实的 rsp 存入 g0.sched.sp,所以看起来貌似还好。不过这个还是比较的 tricky,会不会有其他的坑,其实也说不太好。

1
2
3
4
MOVQ	m_g0(BX), SI
MOVQ (g_sched+gobuf_sp)(SI), AX
MOVQ AX, 0(SP)
MOVQ SP, (g_sched+gobuf_sp)(SI)

又来 revert

而然在 CL 479915 合并之后第二天,Cherry 大佬就来了个 revert CL ,直接把 cgo 优化全 revert 了。原因是新的修复,发现了更多了 breakage。

不过是啥错误,也没有细说,不过,Michael 大佬提了另外一个 CL 481057 ,看起来是内存泄漏导致了 sanitizers 异常。

好在呢,Cherry 大佬又提了个 CL 481061 ,打算重新把 cgo 优化合进来,虽然 Michael 大佬已经给了 +2 approve,但是还没合。

比较让人担心的是,不清楚 google 内部的测试,是否还有其他异常,希望顺利吧 …

最后

其实,之前对于 g0 的理解是不到位的,以为是分配了单独的栈空间,实际上开启 cgo 时 g0 是复用的 c 线程栈。

果然,没理解到位的,终将是要付出代价的。辛苦了大佬们帮忙填坑,哈哈

这是第二次 CL 被 revert 了,真心不好玩,不过,确实是自己没搞好,也挺感谢有这些测试,也很感叹工程化能力真牛,respect!

从去年 3 月 15 日第一次提交,到昨天被合并,预计下一个 1.21 版本可以发布,这个 cgo 优化 搞了一年多,经历了各种波折坎坷,从一个 600 多行的补丁,折腾到了 200 多行,最后到合并的时候,又成了 700 多行,还是挺不容易的。

尤其是,这周是 Go team 的 Quiet Week,一般是不处理外部事务的,但是 Cherry 大佬还是一直在 review,也是挺让我感动的,respect

按照崔老师的话说,这波是逆风局了,因为这个优化可能并不是 Google 内部需要的,或者 Go team 所看重的,能走到现在也是受了很多人的帮助,这里记录下过程,踩过的坑,也对给予过帮助的人,表达谢意

起因

前年底加入 MOSN 团队,开始搞 MoE 以来,就一直有在折腾 cgo,因为 MoE 是重度依赖 cgo,并且是业界少见的,有点把 Go 当做嵌入式,这么频繁的跟 C 宿主交互的玩法。

去年写过这篇文章,详细介绍过这个优化 ,感兴趣的可以去仔细了解。

简单来说,就是 c 调 Go 的时候,需要在 C 线程上伪装 Go runtime 所需要的 GMP 环境,每次从 C 进入 Go,会用 needm 来获取 M 和 g,从 Go 返回 C,会用 dropm 来释放 M 和 g。然而,needmdropm 很重的操作,这个优化也很简单,就是复用,只在 C 线程退出的时候,才释放。

PreBindExtraM C API

起初,对这块机制也不是那么清楚,就搞了个 新增 API 来主动开启优化的提案 ,也就是 C 可以主动调用 PreBindExtraM 来提前绑定 M,然后这个 M 就一直不会被释放了。

经过 ian 和 aclements 两位大佬的提醒,原来注释里 rsc 大佬提了一个 TODO,从 Go 返回 C 的时候,可以不释放 M,但是前提是,需要用 pthread_key_create 注册一个 destructor,在线程退出的时候,可以释放 M,否则,M 就可能会泄漏了。

除了像 Windows 这种,不支持注册 pthread destructor 的系统,都启用优化,也就是第一次获取到 M 之后,就不再释放,直到 C 线程退出。

全部 CPU 体系跑通

接下来的主要工作,主要是通过 destructor,在线程退出的时候,释放 M 了。

因为 destructor 是 os 触发的,使用的是 C function call ABI,但是 dropm 是在一个 Go 函数,自然是 Go function Call ABI,这两者的寄存器使用是两套约定。

所以,参考了 C 调用 Go 会用到的 crosscall2 的实现,引入了 cgodropm 函数,将 C 里面的 callee-save, Go 里面的 caller-save 全部压栈,再调用 dropm。

在 ian 的帮助下,先是跑通了 amd64,然后又跑通了十来个 CPU 体系结构。此时已经是 600 多行的补丁了。

虽然,大部分的汇编是从 crosscall2 抄过来,不过也踩了一些坑,其中印象最深的是 arm64,stack pointer 必须是 16 byte 对齐的,否则会抛 bus error 异常了。

复用 crosscall2

然后是 Michael 大佬来了,他坚持不想要这个大段汇编的 cgodropm,改为复用 crosscall2 + hack cgocallback

这… 几百行的汇编,也没办法,大佬坚持要改,只好重新改了。改完之后,又瘦身到 200 多行的补丁了。

不过,这里依然有一个问题,crosscall2 只是导出给外部 link 的 C 程序使用,并没有导出给 runtime 的 C 程序使用,在这里又开始折腾了好久。

为此研究了一番,Go 和 C 之间的符号导出机制,link 的工作机制。

折腾了一番,搞了个 cgo_crosscall2 的 wrapper 函数,导出给 runtime 的 C 程序使用,但是大佬不认。

runtime C 直接调用 crosscall2

这时 Cherry 大佬出来了,坚持认为应该在 runtime C 代码里面直接调用 crosscall2,如果有问题的话,就是 compiler 或者 link 哪里有问题。

好吧,只提了要求,但是也没有太具体的指导,还是只能自己折腾。

这回是研究了 compiler 的流程,cgo compiler 编译出来的中间结果,会通过 gcc ld 来会判断是否依赖外部符号,原来的 runtime C 是不会引用外部函数的,但是 crosscall2 是在汇编里实现的,在 runtime C 里加了 crosscall2 的调用,这下就捅了马蜂窝了,gcc ld 认为是依赖外部符号的,发生了连锁反应,后面的编译测试都有问题了。

经过了一通折腾,最后在 Cherry 大佬的提议下,搞了一个小 hack,在 cgo compiler 的时候,临时搞个假的 crosscall2 欺骗 gcc ld。

然而,Cherry 大佬最后又反水了,不想这么搞了,因为会在 compiler 流程里,做一些 hardcode 的 hack …

函数指针变量

这时候,Cherry 大佬,希望让我试下,通过汇编将 crosscall2 的函数地址,写入到 C 里面的函数指针变量。

这… 不是跟上一版差不多么?然而,大佬坚持,而且也表达了歉意,那也没辙了,继续折腾。

此时,Go 摸得也差不多,并且大佬也给出了比较具体的建议,所以搞起来,也还算比较顺利。

这一版里,Go 不需要获取 crosscall2 的值,改为提供一个汇编函数,将 crosscall2 写入一个 C 函数指针变量,确实看起来也更干净了。

虽然也费了不少劲,但是还算比较顺利,又通过了测试了。并且 Cherry 和 Ian 两位大佬都给了 approve。

Slow trybots

看起来比较接近了,这次热心网友 thepudds 提议要不要跑一下 slow trybots,也就是一些比较少见的测试环境,包括一些听都没听过的操作系统,CPU 架构。

说到 thepudds,也挺让我感动的,从 PR 一开始就有参与讨论,可前期并没有参与,但是到了后期,我提了 patch,基本很快就帮我 run trybots。

Go 并不是默认就会跑测试,而是需要有权限的人来触发,如果要等 Go team 的人来确认,又是多了一天的往返,所以,有段时间我是请崔老师帮忙,thepudds 热心之后,崔老师也没那么烦我了,哈哈

说到 slow trybots,去年提过一个补丁,合并之后,就是因为 slow trybots 失败了,直接被 revert 了。

有了这种惨痛教训,我也挺希望跑跑 slow trybots,实在不想再被 revert 了。

macOS m1

好吧,果然发现了一些失败,第一个就是 macOS m1。并不是 arm64 都有问题,只有 macOS m1 才能复现…

好在跟公司 IT 临时借了个 m1 …

最后定位是,macOS m1 上,对于 TLS 变量的顺序不太一样,看起来是先清理了 Go 侧的 TLS,然后才调用的 pthread 注册的 destructor …

好吧,又是一通改,那就先把 m 存到 C 侧的 TLS,不再依赖 os 的 clean 顺序了。

AIX ppc64

还有另一个失败,则是完全没听过的 AIX ppc64 环境,这是 IBM 搞的操作系统和 CPU。

只有 AIX 这个系统还有点文档,仔细查阅之后,也没有发现有啥差异 … ppc64 的机器,那是别想搞到手的了 …

就在一筹莫展之际,无意间发现 master 分支上,有一个 ppc64 相关的变更,一看是 ibm 的邮箱,并且还有 +2 的 approve 权限。

尝试发了个邮件,请求帮助,很幸运大佬回复了,在大佬的指引和帮助下,这个兼容性也修复了。

简单来说,AIX 使用的 function call 还是 ELF v1 版本,function call 并不是直接 call 函数地址,而是搞一个 function descriptor。

合并

搞完这些兼容性问题,就是上周了。

等了几天,Cherry 大佬还没回应,这周又 ping 了一次,thepudds 提醒这周是 Quiet Week,心想,好吧,再等一周吧。

很惊喜的是,Cherry 大佬居然出来了,又 review 几轮,基本每天一往返,非常高效。

就在昨天,就这么被合并了,哈哈,真心不容易。

并且,合并之后,还有一个 arm 的 slow trybots 失败了,大佬也很给力,没有 revert,直接帮忙给修了,欧力给!

早上起来发现补丁被合并之后,激动得立马请 chatgpt 写了一段彩虹屁,好好夸了一番,哈哈。

感受

最大的一个感受,be nice!

整体过程中,大佬们大多是不太积极的,不过也可以理解,大佬也是 Google 的打工人,有自己的工作,这种菜鸟提的 patch,谁知道你会不会弃坑呢。

好在从一开始,崔老师就给我打过预防针,这个会很难,有一定的心理预期,所以过程中,肯定有不爽的时候,尤其是各种 ping,发邮件都没有回应的时候,然而,keep nice 我想我还是做到了,哈哈。

好在最后大佬们看到我的耐心,也开始变得积极了,信任也是这么一点点积累出来的。

最后

哈哈,流水账记录了不少,虽然走了不少弯路,但是其中踩过的坑,都是成长,记录下来,也是希望能有一些借鉴意义。

总体来说,这个优化其实也不难,为了搞一个简单的优化,把 compiler,link,runtime 研究了一遍。

有点像,当年搞一个 LuaJIT 的小 bug,把 LuaJIT 的 c code,byte code,IR code,assembly code 研究了一遍。

虽然比较折腾,不过还是蛮有意思的,相信后面还可以给 cgo 搞更多的优化

如果你也重度依赖 cgo 欢迎一起交流~

上一篇我们体验了用 Istio 做控制面,给 Go 扩展推送配置,这次我们来体验一下,在 Go 扩展的异步模式下,对 goroutine 等全部 Go 特性的支持。

异步模式

之前,我们实现了一个简单的 basic auth,但是,那个实现是同步的,也就是说,Go 扩展会阻塞,直到 basic auth 验证完成,才会返回给 Envoy。

因为 basic auth 是一个非常简单的场景,用户名密码已经解析在 Go 内存中了,整个过程只是纯 CPU 计算,所以,这种同步的实现方式是没问题的。

但是,如果我们要实现一个更复杂的需求,比如,我们要将用户名密码,调用远程接口查询,涉及网络操作,这个时候,同步的实现方式就不太合适了。因为,同步模式下,如果我们要等待远程接口返回,那么,Go 扩展就会阻塞,Envoy 也就无法处理其他请求了。

所以,我们需要一种异步模式:

  1. 我们在 Go 扩展中,启动一个 goroutine,然后立即返回给 Envoy,当前正在处理的请求会被挂起,Envoy 则可以继续处理其他请求。
  2. goroutine 在后台异步执行,当 goroutine 中的任务完成之后,再回调通知 Envoy,挂起的请求可以继续处理了。

注意:虽然 goroutine 是异步执行,但是 goroutine 中的代码,与同步模式下的代码,几乎是一样的,并不需要特别的处理。

为什么需要

为什么需要支持 Goroutine 等全部 Go 的特性呢?

有两方面的原因:

  1. 有了 full feature supported Go,我们可以实现很非常强大,复杂的扩展
  2. 可以非常方便的集成现有的 Go 世界的代码,享受 Go 生态的红利
    如果不支持全部的 Go 特性,那么在集成现有 Go 代码的时候,会有诸多限制,导致需要重写大量的代码,这样,就享受不到 Go 生态的红利了。

实现

接下来,我们还是通过一个示例来体验,这次我们实现 basic auth 的远程校验版本,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
go func() {
// verify 中的代码,可以不需要感知是否异步
// 同时,verify 中是可以使用全部的 Go 特性,比如,http.Post
if ok, msg := f.verify(header); !ok {
f.callbacks.SendLocalReply(401, msg, map[string]string{}, 0, "bad-request")
return
}
// 这里是唯一的 API 区别,异步回调,通知 Envoy,可以继续处理当前请求了
f.callbacks.Continue(api.Continue)
}()
// Running 表示 Go 还在处理中,Envoy 会挂起当前请求,继续处理其他请求
return api.Running
}

再来看 verify 的代码,重点是,我们可以在这里使用全部的 Go 特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 这里使用了 http.Post
func checkRemote(config *config, username, password string) bool {
body := fmt.Sprintf(`{"username": "%s", "password": "%s"}`, username, password)
remoteAddr := "http://" + config.host + ":" + strconv.Itoa(int(config.port)) + "/check"
resp, err := http.Post(remoteAddr, "application/json", strings.NewReader(body))
if err != nil {
fmt.Printf("check error: %v\n", err)
return false
}
if resp.StatusCode != 200 {
return false
}
return true
}

// 这里操作 header 这个 interface,与同步模式完全一样
func (f *filter) verify(header api.RequestHeaderMap) (bool, string) {
auth, ok := header.Get("authorization")
if !ok {
return false, "no Authorization"
}
username, password, ok := parseBasicAuth(auth)
if !ok {
return false, "invalid Authorization format"
}
fmt.Printf("got username: %v, password: %v\n", username, password)

if ok := checkRemote(f.config, username, password); !ok {
return false, "invalid username or password"
}
return true, ""
}

另外,我们还需要实现一个简单的 HTTP 服务,用来校验用户名密码,这里就不展开了,用户名密码还是 foo:bar

完整的代码,请移步 github

测试

老规矩,启动之后,我们使用 curl 来测试一下:

1
2
3
4
5
6
$ curl -s -I -HHost:httpbin.example.com "http://$INGRESS_HOST:$INGRESS_PORT/status/200"
HTTP/1.1 401 Unauthorized

# valid foo:bar
$ curl -s -I -HHost:httpbin.example.com "http://$INGRESS_HOST:$INGRESS_PORT/status/200" -H 'Authorization: basic Zm9vOmJhcg=='
HTTP/1.1 200 OK

依旧符合预期。

总结

在同步模式下,Go 代码中常规的异步非阻塞,也会变成阻塞执行,这是因为 Go 和 Envoy 是两套事件循环体系。

而通过异步模式,Go 可以在后台异步执行,不会阻塞 Envoy 的事件循环,这样,就可以用上全部的 Go 特性了。

由于 Envoy Go 暴露的是底层的 API,所以实现 Go 扩展的时候,需要关心同步和异步的区别。

当然,这对于普通的扩展开发而言,并不是一个友好的设计,只所有这么设计,更多是为了极致性能的考量。

大多数场景下,其实并不需要到这么极致,所以,我们会在更上层提供一种,默认异步的模式,这样,Go 扩展的开发者,就不需要关心同步和异步的区别了。

欢迎感兴趣的持续关注~

上一篇我们用 Go 扩展实现了 basic auth,体验了 Go 扩展从 Envoy 接受配置。

只所以这么设计,是想复用 Envoy 原有的 xDS 配置推送通道,这不,今天我们就来体验一番,云原生的配置变更。

前提准备

这次我们需要一套 k8s 环境,如果你手头没有,推荐使用 kind 安装一套。具体安装方式,这里就不展开了。

安装 Istio

我们直接安装最新版的 Istio:

1
2
3
4
5
6
7
8
9
10
# 下载最新版的 istioctl
$ export ISTIO_VERSION=1.18.0-alpha.0
$ curl -L https://istio.io/downloadIstio | sh -

# 将 istioctl 加入 PATH
$ cd istio-$ISTIO_VERSION/
$ export PATH=$PATH:$(pwd)/bin

# 安装,包括 istiod 和 ingressgateway
$ istioctl install

是的,由于 Go 扩展已经贡献给了上游官方,Istiod(pilot)和 ingressgateway 都已经默认开启了 Go 扩展,并不需要重新编译。

Istio 配置 Ingress

我们先用 Istio 完成标准的 Ingress 场景配置,具体可以看 istio 的官方文档

配置好了之后,简单测试一下:

1
2
3
4
$ curl -s -I -HHost:httpbin.example.com "http://$INGRESS_HOST:$INGRESS_PORT/status/200"
HTTP/1.1 200 OK
server: istio-envoy
date: Fri, 10 Mar 2023 15:49:37 GMT

基本的 Ingress 已经跑起来了。

挂载 Golang so

之前我们介绍过,Go 扩展是单独编译为 so 文件的,所以,我们需要把 so 文件,挂载到 ingressgateway 中。

这里我们把上次 basic auth 编译出来的 libgolang.so,通过本地文件挂载进来。简单点搞,直接 edit deployment 加了这些配置:

1
2
3
4
5
6
7
8
9
10
11
12
# 申明一个 hostPath 的 volume
volumes:
- name: golang-so-basic-auth
hostPath:
path: /data/golang-so/example-basic-auth/libgolang.so
type: File

# 挂载进来
volumeMounts:
- mountPath: /etc/golang/basic-auth.so
name: golang-so-basic-auth
readOnly: true

开启 Basic auth 认证

Istio 提供了 EnvoyFilter CRD,所以,用 Istio 来配置 Go 扩展也比较的方便,apply 这段配置,basic auth 就开启了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: golang-filter
namespace: istio-system
spec:
configPatches:
# The first patch adds the lua filter to the listener/http connection manager
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
subFilter:
name: "envoy.filters.http.router"
patch:
operation: INSERT_BEFORE
value: # golang filter specification
name: envoy.filters.http.golang
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config"
library_id: example
library_path: /etc/golang/basic-auth.so
plugin_name: basic-auth
plugin_config:
"@type": "type.googleapis.com/xds.type.v3.TypedStruct"
type_url: typexx
value:
username: foo
password: bar

虽然有点长,但是,也很明显,配置的用户名密码还是:foo:bar

测试

我们测试一下:

1
2
3
4
5
6
$ curl -s -I -HHost:httpbin.example.com "http://$INGRESS_HOST:$INGRESS_PORT/status/200"
HTTP/1.1 401 Unauthorized

# valid foo:bar
$ curl -s -I -HHost:httpbin.example.com "http://$INGRESS_HOST:$INGRESS_PORT/status/200" -H 'Authorization: basic Zm9vOmJhcg=='
HTTP/1.1 200 OK

符合预期。

接下来,我们改一下 EnvoyFilter 中的密码,重新 apply,再测试一下:

1
2
3
# foo:bar not match the new password
$ curl -s -I -HHost:httpbin.example.com "http://$INGRESS_HOST:$INGRESS_PORT/status/200" -H 'Authorization: basic Zm9vOmJhcg=='
HTTP/1.1 401 Unauthorized

此时的 Envoy 并不需要重启,新的配置就立即生效了,云原生的体验就是这么溜~

总结

因为 Go 扩展可以利用 Envoy 原有的 xDS 来接受配置,所以,从 Istio 推送配置也变得很顺利。

不过呢,Istio 提供的 EnvoyFilter CRD 在使用上,其实并不是那么方便 & 自然,后面我们找机会试试 EnvoyGateway,看看 k8s Gateway API 的体验咋样。

至此,我们已经体验了整个 Envoy Go 的开发 & 使用流程,在云原生时代,人均 Golang 的背景下,相信可以很好的完成网关场景的各种定制需求。

下一篇,我们将介绍,如何在 Go 扩展中使用异步协程。这意味着,我们可以使用的是一个全功能的 Go 语言,而不是像 Go Wasm 那样,只能用阉割版的。

敬请期待 ~

去年 10 月搞的提案,最近终于被接受了,第一个被接受的提案,写篇文章记录下的,嘿嘿

故事

去年在搞 MoE,MOSN on Envoy 新架构,折腾了不少 cgo。在分析 cgo 实现的时候,发现了一个 GC 相关的优化点,于是就搞了个提案。

10 月份提交的,一直没动静。今年 2 月开始活跃了,经过简单几轮讨论,就被接受了,整体还算比较顺利的。

之所以会比较顺利,我估计也是搭上了 Go team 的便车,看起来是他们想优化一下 escape analysis,这个提案刚好可以 match,给 escape analysis 更多的信息。

场景

我们知道 C 函数只有一个返回值,Go 调用 C 的时候,如果需要多个返回值,那么 C 函数如何设计呢?

首先,我们希望 Go 和 C 之间的交互足够简单,每次调用都是独立的。比如,我们并不希望在 C 侧申请内存,然后给 Go 返回内存指针。因为这样的话,我们还需要显式释放内存。

那么,最好的办法,就是按照 C 的套路玩,将 Go 对象内存指针,通过参数传给 C,在 C 里给 Go 的内存对象赋值。比如,在 Envoy Go 扩展里,我们就是这么玩的,可以看这个简单的示例:

1
2
3
4
5
6
7
8
9
func (c *httpCApiImpl) HttpGetIntegerValue(r unsafe.Pointer, id int) (uint64, bool) {
var value uint64
res := C.envoyGoFilterHttpGetIntegerValue(r, C.int(id), unsafe.Pointer(&value))
if res == C.CAPIValueNotFound {
return 0, false
}
handleCApiStatus(res)
return value, true
}

这里,我们将一个 uint64 内存对象的地址传给 C,是不是看起来比较清爽了呢。

问题

单从 Go 代码来看,value 是可以放在 stack 上的,但是,由于有了 cgo 调用,目前的实现,会将 value escape 到 heap 上,这会加重 GC 负载。

尤其在 MoE 这种网关场景中,这种代码是跑在请求处理的热路径上的,并且 Go 代码中可能会比较频繁的进行这类调用,也就是有很多这类 GC 对象 escape 到 heap 中,因此造成的 GC 开销,应该也是不小的。

实现

为了确保总是会 escape,cgo compiler 会生成的这样的 Go 包裹代码:

1
2
3
4
5
6
7
func _Cgo_use(interface{})

func _Cfunc_xxx(xxx) {
if _Cgo_always_false {
_Cgo_use(p0)
}
}

目的是,欺骗 escape analysis,如果 p0 是指针,则总是会逃逸。

并且,由于 _Cgo_always_false 总是为假,在编译优化阶段,这个分支又被优化掉了。

原因

为什么有了 cgo 调用,就需要 escape 呢?

Go stack

这里需要先简单介绍一下 Go stack:

目前版本里,Go stack 大小是可变的,而且是连续内存,当生长/缩小的时候,会重新申请内存段,将老的内存从 old stack copy 到 new stack。

并且 copy 过程中会进行比较复杂的栈上指针映射转换,也就是说,stack 上也可以有指针变量指向 stack 上的地址。

但是,如果指针变量已经传给 C,那是没有办法做指针映射转换了,也就是栈发生移动的时候,C 拿到的地址就是非法的了。

时机

那什么情况下,会出现呢?

最开始想到的是,在进入 C 之后,如果 Go 发生了 GC,可能会触发缩栈,但是后来仔细看代码,进入 C 之后,缩栈是被禁掉了的。

另外一个是,不太常用的场景,C 如果又回调了 Go,那么在 Go 里面是可以伸缩栈了,如果在回到 C,此时 C 中的地址就是非法的了。

解决办法

增加 annotation,让编译器感知不需要 escape,不生成 _Cgo_use 就可以了。

一开始我提的是 go:cgo_unsafe_stack_pointer,Go team 觉得不好,最后是 rsc 大佬提的:#cgo noescape/nocallback

感兴趣的话,可以看 proposal 的讨论:
https://github.com/golang/go/issues/56378

最后

提案是接受了,实现还是有些工作量的,主要是这个 annotation 是放在 C code 里的,目前 Go 对于 C 代码并没有 parser。
剩下的部分,应该是比较简单的了。

后面有空在搞了。

上一篇我们用一个简单的示例,体验了用 Golang 扩展 Envoy 的极速上手。

这次我们再通过一个示例,来体验 Golang 扩展的一个强大的特性:从 Envoy 接收配置。

Basic Auth

我们还是从一个小示例来体验,这次我们实现标准的 basic auth 的认证,与上一次示例不同的是,这次认证的用户密码信息,需要从 Envoy 传给 Go,不能在 Go 代码中写死了。

完整的代码可以看 example-basic-auth,下面我们展开介绍一番。

获取配置

为了更加灵活,在设计上,Envoy 传给 Go 的配置是 Protobuf 的 Any 类型,也就是说,配置内容对于 Envoy 是透明的,我们在 Go 侧注册一个解析器,来完成这个 Any 配置的解析。

如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func init() {
// 注册 parser
http.RegisterHttpFilterConfigParser(&parser{})
}

func (p *parser) Parse(any *anypb.Any) interface{} {
configStruct := &xds.TypedStruct{}
if err := any.UnmarshalTo(configStruct); err != nil {
panic(err)
}

v := configStruct.Value
conf := &config{}
if username, ok := v.AsMap()["username"].(string); ok {
conf.username = username
}
if password, ok := v.AsMap()["password"].(string); ok {
conf.password = password
}
return conf
}

这里为了方便,Any 中的类型是 Envoy 定义的 TypedStruct 类型,这样我们可以直接使用现成的 Go pb 库。

值得一提的是,这个配置解析,只有在首次加载的时候需要执行,后续在 Go 使用的是解析后的配置,所以,我们解析到一个 Go map 可以拥有更好的运行时性能。

同时,由于 Envoy 的配置,也是有层级关系的,比如 http-filter, virtual host, router, virtual clusters 这四级,我们也支持这四个层级同时有配置,在 Go 侧来组织 merge。

当然,这个只有在 Go 侧有复杂的 filter 组织逻辑的时候用得上,后面我们在 MOSN 的上层封装的时候,可以看到这种用法,这里暂时不做展开介绍。

认证

具体的 Basic Auth 认证逻辑,我们可以参考 Go 标准 net/http 库中的 BasicAuth 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (f *filter) verify(header api.RequestHeaderMap) (bool, string) {
auth, ok := header.Get("authorization")
if !ok {
return false, "no Authorization"
}
username, password, ok := parseBasicAuth(auth)
if !ok {
return false, "invalid Authorization format"
}
if f.config.username == username && f.config.password == password {
return true, ""
}
return false, "invalid username or password"
}

这里面的 parseBasicAuth 就是从 net/http 库中的实现,是不是很方便呢。

配置

简单起见,这次我们使用本地文件的配置方式。如下是关键的配置:

1
2
3
4
5
6
7
8
9
10
11
12
http_filters:
- name: envoy.filters.http.golang
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
library_id: example
library_path: /etc/envoy/libgolang.so
plugin_name: basic-auth
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
username: "foo"
password: "bar"

这里我们配置了用户名密码:foo:bar

预告一下,下一篇我们会体验通过 Istio 来推送配置,体会一番动态更新配置的全流程。

测试

编译,运行,跟上篇一样,我们还是使用 Envoy 官方提供的镜像即可。

跑起来之后,我们测试一下:

1
2
3
4
5
6
7
8
9
10
$ curl -s -I 'http://localhost:10000/'
HTTP/1.1 401 Unauthorized

# invalid username:password
$ curl -s -I 'http://localhost:10000/' -H 'Authorization: basic invalid'
HTTP/1.1 401 Unauthorized

# valid foo:bar
$ curl -s -I 'http://localhost:10000/' -H 'Authorization: basic Zm9vOmJhcg=='
HTTP/1.1 200 OK

是不是很简单呢,一个标准的 basic-auth 扩展就完成了。

总结

Envoy 是面向云原生的架构设计,提供了配置动态变更的机制,Go 扩展可以从 Envoy 接受配置,也就意味着 Go 扩展也可以很好的利用这套机制。

Go 扩展的开发者,不需要关心配置的动态更新,只需要解析配置即可,非常的方便~

下一篇我们会介绍,配合 Istio 来动态更新用户名密码,体验一番云原生的配置变更体验。

后续还有更多 Golang 扩展的特性介绍,原理解析,以及,更上层的 MOSN 集成体验,欢迎持续关注。

背景

MoE,MOSN on Envoy 是 MOSN 团队提出的技术架构,经过近两年的发展,在蚂蚁内部已经得到了很好的验证;并且去年我们也将底层的 Envoy Go 七层扩展贡献了 Envoy 官方,MOSN 也初步支持了使用 Envoy 作为网络底座的能力。

准备写一系列的文章,逐一介绍这里面的技术,本文是开篇,重点介绍 MoE 中的基础技术,Envoy Go 扩展。

FAQ

开始前,先回答几个基本的问题:

  1. MoE 与 Envoy Go 扩展的区别

    MoE 是技术架构,Envoy Go 扩展是连接 MOSN 和 Envoy 的基础技术

  2. Envoy Go 扩展,与用 Go 来编译 Wasm 有什么区别

    Envoy Go 支持 Go 语言的所有特性,包括 Goroutine,Go Wasm 则只能使用少量的 Go 语言特性,尤其是没有 Goroutine 的支持

  3. Go 是静态链接到 Envoy 么?

    不是的,Go 扩展编译成为 so,Envoy 动态加载 so,不需要重新编译 Envoy

  4. Envoy Go 支持流式处理么?

    支持的。

    由于 Go 扩展提供的是底层的 API,非常的灵活,使用上相对会稍微复杂一些;如果只想简单的使用,可以使用 MOSN 的 filter,后面我们也会介绍。

需求

我们先实现一个小需求,来实际体会一下:

对请求需要进行验签,大致是从 URI 上的某些参数,以及私钥计算一个 token,然后和 header 中的 token 进行对比,对不上就返回 403。

很简单的需求,仅仅作为示例,主要是体验一下过程。

代码实现

完整的代码可以看 envoy-go-filter-example 这个仓库

这里摘录最核心的两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const secretKey = "secret"

func verify(header api.RequestHeaderMap) (bool, string) {
token, ok := header.Get("token")
if ok {
return false, "missing token"
}

path, _ := header.Get(":path")
hash := md5.Sum([]byte(path + secretKey))
if hex.EncodeToString(hash[:]) != token {
return false, "invalid token"
}
return true, ""
}

func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
if ok, msg := verify(header); !ok {
f.callbacks.SendLocalReply(403, msg, map[string]string{}, 0, "bad-request")
return api.LocalReply
}
return api.Continue
}

DecodeHeaders 是扩展 filter 必须实现的方法,我们就是在这个阶段对请求 header 进行校验。

verfiy 是校验函数,这里的 RequestHeaderMap 是 Go 扩展提供的 interface,我们可以通过它来读写 header,其他都是常见的 Go 代码写法。

编译

编译很简单,与常见的 Go 编译一样,这里我们使用 Golang 官方的 docker 镜像来编译:

1
2
3
4
docker run --rm -v `pwd`:/go/src/go-filter -w /go/src/go-filter \
-e GOPROXY=https://goproxy.cn \
golang:1.19 \
go build -v -o libgolang.so -buildmode=c-shared .

Go 编译还是很快的,只需要几秒钟,当前目录下,就会产生一个 libgolang.so 的文件。

反观 Envoy 的编译速度,一次全量编译,动辄几十分钟,上小时的,这幸福感提升了不止一个档次。

运行

我们可以使用 Envoy 官方提供的镜像来运行,如下示例:

1
2
3
4
5
docker run --rm -v `pwd`/envoy.yaml:/etc/envoy/envoy.yaml \
-v `pwd`/libgolang.so:/etc/envoy/libgolang.so \
-p 10000:10000 \
envoyproxy/envoy:contrib-dev \
envoy -c /etc/envoy/envoy.yaml

只需要把上一步编译的 libgolang.soenvoy.yaml 挂载进去就可以了。

值得一提的是,我们需要在 envoy.yaml 配置中启用 Go 扩展,具体是这段配置:

1
2
3
4
5
6
7
http_filters:
- name: envoy.filters.http.golang
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
library_id: example
library_path: /etc/envoy/libgolang.so
plugin_name: example-1

跑起来之后,我们测试一下:

1
2
3
4
5
$ curl 'http://localhost:10000/'
missing token

$ curl -s 'http://localhost:10000/' -H 'token: c64319d06364528120a9f96af62ea83d' -I
HTTP/1.1 200 OK

符合期望,是不是很简单呢

后续

什么?这个示例太简单?

是的,这里主要是体验下开发流程,下篇我们再介绍更高级的玩法:

Go 接受来自 Envoy 侧的配置,异步 Goroutine,以及与 Istio 配合的用法。