返回
Featured image of post Go与云函数(一) serverless流程体验

Go与云函数(一) serverless流程体验

本系列文章记录了我首次使用serverless服务(腾讯云云函数)来部署Golang后端服务的过程。第一章记录了整个serverless流程的走通——即在本地编写一个程序并以web函数或是事件函数的方式部署在serverless平台上。

腾讯云函数 Go程序开发

比起云服务器,serverless的抽象程度更高了,使用者不再需要了解服务器底层的一些细节,只需要将处理某类事务或者响应某类事件的程序打包好之后上传到serverless平台上,在预定义的事件发生时,该程序便会被触发,对请求或者事件做出响应,可以说是真正的做到了按量付费,提高了服务器的利用率(对云服务提供商),也降低了成本。

下面这张图我觉得非常形象的说明了云计算的发展:

云服务的发展
云服务的发展
之前我只是听说过这么个东西,但一直没有机会来实践。而最近要上线的业务决定采用serverless的方式来部署,正好可以学习一下serverless服务的使用,从腾讯云的云函数入手,写下这篇文章记录一下摸索的过程。

云函数流程走通

先创建了一个云函数

创建云函数
创建云函数
这里我先采用代码部署,运行环境设置为Go1,然后设置为事件函数

我一开始使用的是web函数,但是无论代码怎么写(参考官方文档使用sdk来作为函数入口,或者是自己开一个http服务器监听在9000端口)都无法成功部署,始终提示405错误,容器无法初始化…不知道是我的问题还是怎么。

关于在云函数中使用golang可以参考 https://cloud.tencent.com/document/product/583/18032

我这里犯了一个低级错误,上面的demo是针对事件触发器来写的,而我的需求中使用的是web触发,结果半天没发现这个问题,还一直在想到底哪里错了,怎么这个一直找不到程序入口,容器启动失败呢?

如果要采用web触发的话,需要程序自己实现一个http服务器监听9000端口,对请求做出响应。 demo可以参考 WebFunc-Go1-HttpForwarding

令人尴尬的是,我在采用自己开启一个http服务器并监听的方式后依旧无法启动函数,上面的做法也行不通,如果要用web触发大概是需要自己提供镜像来运行? (经过后续的研究,最终还是成功启动了,见下文)

事件触发函数

我先用事件触发的函数来部署服务。

先用官方文档中的示例代码:

package main

import (
    "context"
    "fmt"
    "github.com/tencentyun/scf-go-lib/cloudfunction"
)

type DefineEvent struct {
    // test event define
    Key1 string `json:"key1"`
    Key2 string `json:"key2"`
}

func hello(ctx context.Context, event DefineEvent) (string, error) {
    fmt.Println("key1:", event.Key1)
    fmt.Println("key2:", event.Key2)
    return fmt.Sprintf("Hello %s!", event.Key1), nil
}

func main() {
    // Make the handler available for Remote Procedure Call by Cloud Function
    cloudfunction.Start(hello)
}

接着构建并上传该程序

Golang 环境的云函数,仅支持 zip 包上传,您可以选择使用本地上传 zip 包或通过 COS 对象存储引用 zip 包。zip 包内包含的应该是编译后的可执行二进制文件。

linux下

GOOS=linux GOARCH=amd64 go build -o main main.go
zip main.zip main

windows下

set GOOS=linux
set GOARCH=amd64
go build -o main main.go

然后还需要压缩成zip包再上传(大概是考虑到了某些程序还会引用一些同目录下的文件,所以需要打包后再上传吧) 并且修改执行方法为可执行文件的文件名,程序的构建和发布还是有点麻烦的,不过之后应该能配合Actions来或者vscode插件简化。

上传完可执行文件后,还可以在高级设置里面设置一下分配的资源之类的属性。在部署之后我们便可以发起一次请求看看是否能得到响应了。

接着我们需要将这个请求的接口暴露出去,对外提供访问,这时需要我们创建一个api网关。

如果当前通过 API 网关控制台配置的 API 网关触发器,处理响应的方式默认为透传响应。如需开启集成响应,请在 API 配置中的后端配置位置,勾选启用集成响应,并在代码中按如下说明的数据结构返回内容。

如果当前通过云函数控制台配置的 API 网关触发器,默认已开启集成响应功能,请注意返回数据的格式

我是在云函数控制台创建的api网关触发器,所以采用的是集成响应的方式,此时我们的响应还会经过api网关的处理。我们的serverless服务需要按照api网关要求的数据格式返回,不然会提示 Invalid scf response. expected scf response valid JSON

集成响应/请求模式下api网关会对请求或响应做出处理,http请求体会被包裹在一个结构里面作为参数传给go程序,格式详见文档

如果嫌麻烦也可以去api网关设置里面把响应集成关了,此时响应便不再经过api网关处理,而是直接由后端程序返回给api请求者。

imageb3d9ee9111032c3a.png
imageb3d9ee9111032c3a.png
既然有“透传响应”那么当然也有“透传请求”,但是“透传请求”属于web触发函数的特性,而我始终无法在web触发函数的方式下运行起go程序,所以这里暂时就没有测试了。

package main
// 下面是集成响应的后端代码
import (
	"context"
	"fmt"

	"github.com/tencentyun/scf-go-lib/cloudevents/scf"
	"github.com/tencentyun/scf-go-lib/cloudfunction"
)

type DefineEvent struct {
	// test event define
	Key1 string `json:"key1"`
	Key2 string `json:"key2"`
}

func hello(ctx context.Context, event DefineEvent) (scf.APIGatewayProxyResponse, error) {
	fmt.Println("key1:", event.Key1)
	fmt.Println("key2:", event.Key2)
	headers := make(map[string]string)
	headers["Content_Type"] = "application/json"
	apiGWResp := scf.APIGatewayProxyResponse{
		StatusCode:      200,
		Body:            fmt.Sprintf("Hello youer request\nctx:%v\nevent:%v", ctx, event),
		Headers:         headers,
		IsBase64Encoded: false,
	}
	return apiGWResp, nil
}

func main() {

	cloudfunction.Start(hello)
}

web函数

为什么会无法执行

折腾了一下午后,我终于搞明白了为什么我的web函数会执行失败,有两个原因。

  1. 启动命令错误
  2. 文件丢失可执行权限

web函数的启动脚本并不能像事件触发函数那样直接填可执行名字的名字,查看启动文件说明可知,我们还需要创建一个脚本文件 scf_bootstrap 作为启动文件,内容很简单,就是一个简单的shell脚本,该脚本要和可执行文件一起打包。

#!/bin/bash
./main # 可执行文件名叫main

你还可以进行一些其他的操作,但是需要注意 在腾讯云标准环境下,仅 /tmp 目录可读可写,输出文件时请注意选择 /tmp 路径,否则会导致服务因缺少写权限而异常退出。。我一开始采用直接填写可执行文件名字的方式,当然是无法启动服务的。

但是在我修改了启动脚本后,发现出现了新的报错 /var/user/scf_bootstrap: line 3: ./main: Permission denied 在 scf_bootstrap里面加入一行 ls -lh 一看,这个可执行文件居然没有可执行权限…

-rw-r–r– 1 qcloud qcloud 5.8M Dec 28 09:55 main

-rwxr-xr-x 1 qcloud qcloud 25 Dec 28 10:12 scf_bootstrap

我采取的开发方式是在windows下通过交叉编译的方式生成linux下的可执行文件并打包上传到scf平台上,但是这样会导致可执行文件没有执行的权限,运行自然也就出错了。为了确认是这个原因,我在linux下 chmod +x main 并打包,再上传到scf上,这次果然正常的运行了。

我们需要知道,在linux ext4文件系统下,文件和目录的权限记录在inode中,但是windows ntfs文件系统中并没有这样的结构,所以我们没办法在windows环境下给与一个linux可执行文件执行权限,并且如果一个具有执行权限的文件被复制到windows下,其权限也会丢失(在归档或者压缩后再复制则不会丢失这些信息)。

而腾讯云的环境,可执行文件所在的目录又是一个只读的目录,我们也就没办法直接在 scf_bootstrap 文件中添加chmod命令去修改文件权限。所以如果开发环境是windows,这里是个不小的坑。我想到的方法是借助github actions这样的系统,在linux的环境下构建并给与执行权限再打包发布。

另一个更加简单的解决方式就是直接把可执行文件也复制到/tmp下,再给与执行权限并运行。

#!/bin/bash
# scf_bootstrap
cp main /tmp/
chmod +x /tmp/main
/tmp/main

一个简单的示例

在搞定了无法运行的问题之后,我构建了一个简单的http服务器并部署到scf上。

package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", HelloServer)
	http.ListenAndServe(":9000", nil)
}

func HelloServer(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
}

构建方式和前面的一样,除此之外还需要一个 scf_bootstrap , 前面也提出了,用chmod +X main给与执行权限后将这两个文件压缩成zip并上传。

最后终于是运行起来了
最后终于是运行起来了

web函数or事件函数

另外说一下我对 web函数和事件函数粗浅的理解 (也不知道是否正确)

在web函数的模式下,请求直接到达我们的服务端,而不经过api网关的包装加工,此时我们的程序需要自己开启一个http server监听在9000端口上,而api网关只是起了一个反向代理的作用,将我们的接口暴露出去。在这种模式下我们用更加传统的方式来开发,gin等框架也不用做出什么修改便能使用,不过这样的话,每个函数我们都得实现一个http服务器,另外启动http服务器可能也会增加消耗,但是这种方式是和云平台比较解耦的,即使我们把程序运行在我们自己的服务器,设置好nginx反向代理后,依旧可以使用之前编写的程序。

而在事件函数模式下,我们的程序中只有处理一个事件/传入的数据的方法,而不需要自己再开一个http server来监听,此时外部请求到达api网关后,api网关会将其包装成一个结构体,并按照规则来路由请求找到对应的处理方法,然后调用我们编写的方法,将外部的请求以方法的参数的形式传入。这个模式看上去是更符合 “云函数” 这个说法的,在这个模式下我们交给云平台的就是一个个等待调用“函数”了,这种方式和平台绑定得更紧密,而且看上去也不是很适合本地调试?并且由于不再是开启一个http服务器并接受请求的方式提供服务,之前的gin http框架大概是用不了了。

在我看来,web函数要更利于本地调试,并且之前的gin等框架的使用经验也可以继续沿用,所以在当前提供一个RESTful API的需求中,web函数是更合适的。而事件函数可能更适合与监听到特定的事件(如文件上传,时间到达)发生时,调用一个函数对事件进行处理,如加工文件或者是定时收集处理日志。 其实我有一点好奇有没有更加通用的术语来描述“web函数”与“事件函数”,另外就是AWS的Lambda是否也做了这样的区分,或者提供了类似的两种不同的调用方法。

到此为止,算是体验了通过云函数来部署一个后端服务的流程。但是要用在实际生产中,还有许多要解决的问题,请看后续的文章。

Licensed under CC BY-NC-SA 4.0
最后更新于 Dec 28, 2021 22:30 +0800
comments powered by Disqus
本站访客数:
Built with Hugo
Theme Stack designed by Jimmy