全部产品
云市场

基于custom runtime 打造 golang runtime

更新时间:2019-10-11 10:07:35

前言

Custom Runtime 手册 阐释了 Custom Runtime 的原理和一些规范,为了加深用户对 Custom Runtime 手册的理解, 本文通过一步一步搭建 golang Runtime 为例, 逐步揭开 Custom Runtime 的面纱。

设置 HTTP 触发器的函数

如手册所言,对于 HTTP Trigger的应用,基于 Custom Runtime 可以开发或者快速移植一个已有的 web App, 比如一个最简单的 web:

  1. package main
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "net/http"
  6. "os"
  7. )
  8. func handler(w http.ResponseWriter, req *http.Request) {
  9. requestID := req.Header.Get("x-fc-request-id")
  10. fmt.Println(fmt.Sprintf("FC Invoke Start RequestId: %s", requestID))
  11. defer func() {
  12. fmt.Println(fmt.Sprintf("FC Invoke End RequestId: %s", requestID))
  13. }()
  14. // your logic
  15. b, err := ioutil.ReadAll(req.Body)
  16. if err != nil {
  17. panic(err)
  18. }
  19. info := fmt.Sprintf("method = %+v;\nheaders = %+v;\nbody = %+v", req.Method, req.Header, string(b))
  20. w.Write([]byte(fmt.Sprintf("Hello, golang http invoke! detail:\n %s", info)))
  21. }
  22. func main() {
  23. fmt.Println("FunctionCompute go runtime inited.")
  24. http.HandleFunc("/", handler)
  25. port := os.Getenv("FC_SERVER_PORT")
  26. if port == "" {
  27. port = "9000"
  28. }
  29. http.ListenAndServe(":" + port, nil)
  30. }

将上述文件编译成可执行文件

注: 基于custom runtime 系统环境 debian9, 建议使用 golang:1.12.9-stretch, docker pull golang:1.12.9-stretch, 具体系统详情参考 Custom Runtime 手册)

可执行文件直接命名为 bootstrap, 然后将bootstrap 文件打包成 zip 文件 code.zip, 然后直接使用 fun 部署即可:

template.yml 示例

  1. ROSTemplateFormatVersion: '2015-09-01'
  2. Transform: 'Aliyun::Serverless-2018-04-03'
  3. Resources:
  4. auto-op-demo-pro:
  5. Type: 'Aliyun::Serverless::Log'
  6. Properties:
  7. Description: 'custom runtime log pro'
  8. fc-log:
  9. Type: 'Aliyun::Serverless::Log::Logstore'
  10. Properties:
  11. TTL: 362
  12. ShardCount: 1
  13. CRService:
  14. Type: 'Aliyun::Serverless::Service'
  15. Properties:
  16. Description: 'custom runtime demo'
  17. Policies:
  18. - AliyunOSSFullAccess
  19. LogConfig:
  20. Project: 'my-log-pro'
  21. Logstore: 'fc-log'
  22. hello:
  23. Type: 'Aliyun::Serverless::Function'
  24. Properties:
  25. Handler: index.handler
  26. CodeUri: ./code.zip
  27. Description: 'demo with custom runtime'
  28. Runtime: custom
  29. Events:
  30. functionA:
  31. Type: HTTP
  32. Properties:
  33. AuthType: ANONYMOUS
  34. Methods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD']
  35. goexample.abc.cn:
  36. Type: 'Aliyun::Serverless::CustomDomain'
  37. Properties:
  38. Protocol: HTTP
  39. RouteConfig:
  40. Routes:
  41. '/*':
  42. ServiceName: CRService
  43. FunctionName: hello

注: Handler 在此时没有实质意义, 填写任意的一个满足 FC handler 字符集约束的字符串即可, 比如 index.handler, 同时在没有设置自定义域名的情况下,直接使用 fc endpoint 的 url 进行访问的时候, path 会是 /2016-08-15/proxy/$serviceName/$functionName/$yourRealPath 这种格式

普通函数

诚然,普通函数调用也可以使用上面使用 server 代码和逻辑绑定的方式去实现,但是每次编写函数都带上一堆 http server 的处理代码是冗余和丑陋的, 接下来基于一个简单的框架 golang-runtime 来编写函数, 只需要函数的 handler 和 initialize 然后再 main函数里面将 handler 和 initialize 传入即可:

其中 handler 和 initialize 定义和官方其他 runtime 类似:

  1. func handler(ctx *FCContext, event []byte) ([]byte, error)
  2. func initialize(ctx *gr.FCContext) error
  1. package main
  2. import (
  3. "encoding/json"
  4. gr "github.com/awesome-fc/golang-runtime"
  5. )
  6. func initialize(ctx *gr.FCContext) error {
  7. fcLogger := gr.GetLogger().WithField("requestId", ctx.RequestID)
  8. fcLogger.Infoln("init golang!")
  9. return nil
  10. }
  11. func handler(ctx *gr.FCContext, event []byte) ([]byte, error) {
  12. fcLogger := gr.GetLogger().WithField("requestId", ctx.RequestID)
  13. b, err := json.Marshal(ctx)
  14. if err != nil {
  15. fcLogger.Error("error:", err)
  16. }
  17. fcLogger.Infof("hello golang! \ncontext = %s", string(b))
  18. return event, nil
  19. }
  20. func main() {
  21. gr.Start(handler, initialize)
  22. }

将上述文件编译成可执行文件

注: 基于custom runtime 系统环境 debian9, 建议使用 golang:1.12.7-stretch, docker pull golang:1.12.7-stretch, 具体系统详情参考 Custom Runtime 手册)

可执行文件直接命名为 bootstrap, 然后将bootstrap 文件打包成 zip 文件 code.zip, 然后直接使用 fun 部署即可:

template.yml 示例:

  1. ROSTemplateFormatVersion: '2015-09-01'
  2. Transform: 'Aliyun::Serverless-2018-04-03'
  3. Resources:
  4. auto-op-demo-pro:
  5. Type: 'Aliyun::Serverless::Log'
  6. Properties:
  7. Description: 'custom runtime log pro'
  8. fc-log:
  9. Type: 'Aliyun::Serverless::Log::Logstore'
  10. Properties:
  11. TTL: 362
  12. ShardCount: 1
  13. CRService:
  14. Type: 'Aliyun::Serverless::Service'
  15. Properties:
  16. Description: 'custom runtime demo'
  17. Policies:
  18. - AliyunOSSFullAccess
  19. LogConfig:
  20. Project: 'auto-op-demo-pro'
  21. Logstore: 'fc-log'
  22. hello:
  23. Type: 'Aliyun::Serverless::Function'
  24. Properties:
  25. Handler: index.handler
  26. Initializer: index.initializer
  27. CodeUri: ./hello.zip
  28. Description: 'demo with custom runtime'
  29. Runtime: custom

注: HandlerInitializer 在此时没有实质意义, 填写任意的一个满足 FC handler 或 Initializer 字符集约束的字符串即可, 比如 index.handlerindex.initializer

如果不需要 initialize, 则不需要编写 initialize 函数,同时main 函数里面的逻辑修改为:

  1. gr.Start(handler, nil)

同时创建函数的时候, 没有 initialize 参数。

详情可以直接参考源码 golang-runtime, 下面简略说明针对手册的一些实现:

1. x-fc-control-path 区分是 handler 还是 initialize

  1. controlPath := req.Header.Get(fcControlPath)
  2. if controlPath == "/initialize" {
  3. initializeHandler(w, req)
  4. } else {
  5. invokeHandler(w, req)
  6. }

2. construct param context and event

  1. ctx := &FCContext{
  2. RequestID: req.Header.Get(fcRequestID),
  3. Credentials: Credentials{
  4. AccessKeyID: req.Header.Get(fcAccessKeyID),
  5. AccessKeySecret: req.Header.Get(fcAccessKeySecret),
  6. SecurityToken: req.Header.Get(fcSecurityToken),
  7. },
  8. Function: FunctionMeta{
  9. Name: req.Header.Get(fcFunctionName),
  10. Handler: req.Header.Get(fcFunctionHandler),
  11. Memory: m,
  12. Timeout: t,
  13. Initializer: req.Header.Get(fcFunctionInitializer),
  14. InitializationTimeout: it,
  15. },
  16. Service: ServiceMeta{
  17. ServiceName: req.Header.Get(fcServiceName),
  18. LogProject: req.Header.Get(fcServiceLogProject),
  19. LogStore: req.Header.Get(fcServiceLogstore),
  20. Qualifier: req.Header.Get(fcQualifier),
  21. VersionID: req.Header.Get(fcVersionID),
  22. },
  23. Region: req.Header.Get(fcRegion),
  24. AccountID: req.Header.Get(fcAccountID),
  25. }
  1. event, err := ioutil.ReadAll(req.Body)
  2. if err != nil {
  3. panic(err)
  4. }

3. x-fc-status 和 日志 Format

  1. func invokeHandler(w http.ResponseWriter, req *http.Request) {
  2. requestID := req.Header.Get(fcRequestID)
  3. fmt.Println(fmt.Sprintf(fcLogTailStartPrefix, requestID))
  4. defer func() {
  5. if r := recover(); r != nil {
  6. w.Header().Set(fcStatus, "404")
  7. w.Write([]byte(fmt.Sprintf("Error: %+v;\nStack: %s", r, string(debug.Stack()))))
  8. }
  9. fmt.Println(fmt.Sprintf(fcLogTailEndPrefix, requestID))
  10. }()
  11. ...
  12. w.Write([]byte(resp))
  13. }

x-fc-status 用于custom runtime 捕获函数代码逻辑异常

通过 404 告知 FC 系统函数执行发生错误, 这样FC 最后会告知调用函数 client 一个 UnHandled Error, 和官方 runtime 行为一致

支持函数调用返回的 reponse 携带函数执行日志

支持调用函数的时候,request 携带 header x-fc-log-type = “Tail”,那么函数的执行日志能通过 response 的 x-fc-log-result header 返回

FC Invoke Start RequestId: ${RequestId}FC Invoke End RequestId: ${RequestId} 这两条日志在每一个 request 的开始和结束是必需的

推荐的日志格式, 带有 UTC 时间和 requestId

  1. log = &logrus.Logger{
  2. Out: os.Stderr,
  3. Level: logrus.InfoLevel,
  4. Formatter: &UTCFormatter{
  5. TimestampFormat: "2006-01-02T15:04:05.999Z",
  6. LogFormat: "%time%: %requestId% [%lvl%] %msg%\n",
  7. },
  8. },
  9. }