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

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

《MoE 系列(三)|使用 Istio 动态更新 Go 扩展配置》中我们体验了用 Istio 做控制面,给 Go 扩展推送配置,这次我们来体验一下,在 Go 扩展的异步模式下,对 Goroutine 等全部 Go 特性的支持。

异步模式

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

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

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

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

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

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

为什么需要

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

有两方面的原因:

  • 有了 Full-feature supported Go,我们可以实现非常强大、复杂的扩展。
  • 可以非常方便的集成现有 Go 世界的代码,享受 Go 生态的红利。

如果不支持全部的 Go 特性,那么在集成现有 Go 代码的时候,会有诸多限制,导致需要重写大量的代码,这样,就享受不到 Go 生态的红利了。

实现

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

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 特性:

// 这里使用了 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[2]。

测试

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

$ 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 扩展的开发者,就不需要关心同步和异步的区别了。

下一篇我们将介绍 Envoy Go 扩展之内存安全。欢迎感兴趣的持续关注~

敬请期待:MoE 系列(五)|Envoy Go 扩展之内存安全

[1]Basic Auth:

https://uncledou.site/2023/moe-extend-envoy-using-golang-2/

[2]Github:

https://github.com/doujiang24/envoy-go-filter-example/tree/master/example-remote-basic-auth

相关文章
|
5月前
|
缓存 NoSQL Go
通过 SingleFlight 模式学习 Go 并发编程
通过 SingleFlight 模式学习 Go 并发编程
|
2月前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。
|
3月前
|
安全 Go 调度
探索Go语言的并发模式:协程与通道的协同作用
Go语言以其并发能力闻名于世,而协程(goroutine)和通道(channel)是实现并发的两大利器。本文将深入了解Go语言中协程的轻量级特性,探讨如何利用通道进行协程间的安全通信,并通过实际案例演示如何将这两者结合起来,构建高效且可靠的并发系统。
|
3月前
|
安全 Go 开发者
破译Go语言中的并发模式:从入门到精通
在这篇技术性文章中,我们将跳过常规的摘要模式,直接带你进入Go语言的并发世界。你将不会看到枯燥的介绍,而是一段代码的旅程,从Go的并发基础构建块(goroutine和channel)开始,到高级模式的实践应用,我们共同探索如何高效地使用Go来处理并发任务。准备好,让Go带你飞。
|
7月前
|
Go
go的并发初体验、加锁、异步
go的并发初体验、加锁、异步
|
3月前
|
安全 编译器 Go
Go runtime 调度器精讲(十):异步抢占
Go runtime 调度器精讲(十):异步抢占
|
5月前
|
存储 Unix 测试技术
解释Go中常见的I/O模式
解释Go中常见的I/O模式
|
5月前
|
Go 数据库 UED
[go 面试] 同步与异步:程序执行方式的不同之处
[go 面试] 同步与异步:程序执行方式的不同之处
|
6月前
|
设计模式 Go
Go语言设计模式:使用Option模式简化类的初始化
在Go语言中,面对构造函数参数过多导致的复杂性问题,可以采用Option模式。Option模式通过函数选项提供灵活的配置,增强了构造函数的可读性和可扩展性。以`Foo`为例,通过定义如`WithName`、`WithAge`、`WithDB`等设置器函数,调用者可以选择性地传递所需参数,避免了记忆参数顺序和类型。这种模式提升了代码的维护性和灵活性,特别是在处理多配置场景时。
79 8