RSS

MoE 系列 [四] - Go 扩展的异步模式

这次我们来体验一下,在 Go 扩展的异步模式下,对 goroutine 等全部 Go 特性的支持

上一篇我们体验了用 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 的远程校验版本,关键代码如下:

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

测试

老规矩,启动之后,我们使用 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 扩展的开发者,就不需要关心同步和异步的区别了。

欢迎感兴趣的持续关注~