真·深入底层原理

本文章以openwhisk平台为例

首先,当客户端的请求,经过平台组件的层层处理和传递(这个过程时间很快),到达openwhisk的invoker组件,它会调用docker对容器进行比较直接的管理。

首先,根据用户上传的action的语言,平台会选择相匹配的语言runtime(运行时,即为某一个语言对应的各种依赖,且这个runtime会被打包成容器镜像,供以启动容器),invoker会调用docker,使用docker run来启动容器。

那容器是怎么对我们上传的源码进行处理,将其运行,并最终将运行结果返回给我们的呢。

以go语言为例

go语言的runtime,也就是对应的镜像,其docker file内容如下

官方go运行时链接

#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

# Do not fix the patch level for golang:1.19 to automatically get security fixes.
FROM golang:1.19-bullseye

RUN echo "deb http://deb.debian.org/debian buster-backports main contrib non-free" \
     >>/etc/apt/sources.list &&\
    echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections &&\
    apt-get update &&\
    # Upgrade installed packages to get latest security fixes if the base image does not contain them already.
    apt-get upgrade -y --no-install-recommends &&\
    apt-get install -y apt-utils &&\
    apt-get install -y \
     curl \
     jq \
     git \
     zip \
     vim && \
     apt-get -y install \
     librdkafka1 \
     librdkafka++1 &&\
    apt-get -y install \
     librdkafka-dev &&\
    # Cleanup apt data, we do not need them later on.
    apt-get clean && rm -rf /var/lib/apt/lists/* &&\
    go install github.com/go-delve/delve/cmd/dlv@latest &&\
    #Update net Package as temporary vulnerability fix
    cd /usr/local/go/src/ &&\
    go get -u golang.org/x/net@v0.8.0 &&\
    mkdir /action
#make python 3 react as python
RUN ln -s /usr/bin/python3 /usr/bin/python

WORKDIR /action
ADD proxy /bin/proxy
ADD bin/compile /bin/compile
ADD lib/launcher.go /lib/launcher.go
ENV OW_COMPILER=/bin/compile
ENV OW_LOG_INIT_ERROR=1
ENV OW_WAIT_FOR_ACK=1
ENV OW_EXECUTION_ENV=openwhisk/action-golang-v1.19
ENTRYPOINT [ "/bin/proxy" ]

对该Dockerfile进行分析

我们知道,它在golang:1.19-bullseye镜像的基础上,

基于这个镜像创建的容器就相当于我们启动了一个。。。。这样的虚拟机

docker创建这个容器之后,容器内部会运行/bin/proxy这个进程

容器启动之后

经过上面的介绍,我们知道容器内部有哪些依赖和资源,以及容器刚启动时运行的是/bin/proxy进程,那么,这个proxy进程都干什么呢,以及上传的action代码在容器内部是如何处理的呢

下面我们看一下其主函数

package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"runtime"

	"github.com/apache/openwhisk-runtime-go/openwhisk"
)

// flag to show version
var version = flag.Bool("version", false, "show version")

// flag to enable debug
var debug = flag.Bool("debug", false, "enable debug output")

// flag to require on-the-fly compilation
var compile = flag.String("compile", "", "compile, reading in standard input the specified function, and producing the result in stdout")

// flag to pass an environment as a json string
var env = flag.String("env", "", "pass an environment as a json string")

// fatal if error
func fatalIf(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

func main() {
	flag.Parse()

	// show version number
	if *version {
		fmt.Printf("OpenWhisk ActionLoop Proxy v%s, built with %s\n", openwhisk.Version, runtime.Version())
		return
	}

	// debugging
	if *debug {
		// set debugging flag, propagated to the actions
		openwhisk.Debugging = true
		os.Setenv("OW_DEBUG", "1")
	}

	// create the action proxy
	ap := openwhisk.NewActionProxy("./action", os.Getenv("OW_COMPILER"), os.Stdout, os.Stderr)

	// compile on the fly upon request
	if *compile != "" {
		ap.ExtractAndCompileIO(os.Stdin, os.Stdout, *compile, *env)
		return
	}

	// start the balls rolling
	openwhisk.Debug("OpenWhisk ActionLoop Proxy %s: starting", openwhisk.Version)
	ap.Start(8080)

}

比较核心的为

ap := openwhisk.NewActionProxy("./action", os.Getenv("OW_COMPILER"), os.Stdout, os.Stderr)
xxxx
xxx
xxx
xxx
ap.Start(8080)

查看ap.Start的实现,我们找到

// Start creates a proxy to execute actions
func (ap *ActionProxy) Start(port int) {
	// listen and start
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), ap))
}

看到上面的这些代码,我们就知道了,大致就是创建一个服务器,然后监听8080端口(容器内部的8080端口)。

当收到请求之后

上面的http.ListenAndServe函数中,第二个参数为Handler类型,该类型是一个接口,要求为“实现了ServeHTTP”的类。且当请求到来之后,会调用该类的ServeHTTP方法。

所以,当请求到来之后,请求的数据,会由 ap变量类型(ActionProxy)的ServerHTTP方法来处理。

接下来,我们找到该方法

func (ap *ActionProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	switch r.URL.Path {
	case "/init":
		ap.initHandler(w, r)
	case "/run":
		ap.runHandler(w, r)
	}
}

可以看到,proxy会根据请求为init或run,进行对应的处理。

而对于第一次到来的请求,invoker会先发起init请求,init结束后发送run请求

init操作会做什么

以下为init函数的完整代码

func (ap *ActionProxy) initHandler(w http.ResponseWriter, r *http.Request) {

	// you can do multiple initializations when debugging
	if ap.initialized && !Debugging {
		msg := "Cannot initialize the action more than once."
		sendError(w, http.StatusForbidden, msg)
		log.Println(msg)
		return
	}

	// read body of the request
	if ap.compiler != "" {
		Debug("compiler: " + ap.compiler)
	}

	body, err := ioutil.ReadAll(r.Body)
	defer r.Body.Close()
	if err != nil {
		sendError(w, http.StatusBadRequest, fmt.Sprintf("%v", err))
		return
	}

	// decode request parameters
	if len(body) < 1000 {
		Debug("init: decoding %s\n", string(body))
	}

	var request initRequest
	err = json.Unmarshal(body, &request)

	if err != nil {
		sendError(w, http.StatusBadRequest, fmt.Sprintf("Error unmarshaling request: %v", err))
		return
	}

	// request with empty code - stop any executor but return ok
	if request.Value.Code == "" {
		sendError(w, http.StatusForbidden, "Missing main/no code to execute.")
		return
	}

	// passing the env to the action proxy
	ap.SetEnv(request.Value.Env)

	// setting main
	main := request.Value.Main
	if main == "" {
		main = "main"
	}

	// extract code eventually decoding it
	var buf []byte
	if request.Value.Binary {
		Debug("it is binary code")
		buf, err = base64.StdEncoding.DecodeString(request.Value.Code)
		if err != nil {
			sendError(w, http.StatusBadRequest, "cannot decode the request: "+err.Error())
			return
		}
	} else {
		Debug("it is source code")
		buf = []byte(request.Value.Code)
	}

	// if a compiler is defined try to compile
	_, err = ap.ExtractAndCompile(&buf, main)
	if err != nil {
		if os.Getenv("OW_LOG_INIT_ERROR") == "" {
			sendError(w, http.StatusBadGateway, err.Error())
		} else {
			ap.errFile.Write([]byte(err.Error() + "\n"))
			ap.outFile.Write([]byte(OutputGuard))
			ap.errFile.Write([]byte(OutputGuard))
			sendError(w, http.StatusBadGateway, "The action failed to generate or locate a binary. See logs for details.")
		}
		return
	}

	// start an action
	err = ap.StartLatestAction()
	if err != nil {
		if os.Getenv("OW_LOG_INIT_ERROR") == "" {
			sendError(w, http.StatusBadGateway, "cannot start action: "+err.Error())
		} else {
			ap.errFile.Write([]byte(err.Error() + "\n"))
			ap.outFile.Write([]byte(OutputGuard))
			ap.errFile.Write([]byte(OutputGuard))
			sendError(w, http.StatusBadGateway, "Cannot start action. Check logs for details.")
		}
		return
	}
	ap.initialized = true
	sendOK(w)
}

action只包含单个可执行文件的情况下,action的源码会传递给init函数,并进行编译。 

run操作会做什么

func (ap *ActionProxy) runHandler(w http.ResponseWriter, r *http.Request) {

	// parse the request
	body, err := ioutil.ReadAll(r.Body)
	defer r.Body.Close()
	if err != nil {
		sendError(w, http.StatusBadRequest, fmt.Sprintf("Error reading request body: %v", err))
		return
	}
	Debug("done reading %d bytes", len(body))

	// check if you have an action
	if ap.theExecutor == nil {
		sendError(w, http.StatusInternalServerError, fmt.Sprintf("no action defined yet"))
		return
	}
	// check if the process exited
	if ap.theExecutor.Exited() {
		sendError(w, http.StatusInternalServerError, fmt.Sprintf("command exited"))
		return
	}

	// remove newlines
	body = bytes.Replace(body, []byte("\n"), []byte(""), -1)

	// execute the action
	response, err := ap.theExecutor.Interact(body)

	// check for early termination
	if err != nil {
		Debug("WARNING! Command exited")
		ap.theExecutor = nil
		sendError(w, http.StatusBadRequest, fmt.Sprintf("command exited"))
		return
	}
	DebugLimit("received:", response, 120)

	// check if the answer is an object map
	var objmap map[string]*json.RawMessage
	var objarray []interface{}
	err = json.Unmarshal(response, &objmap)
	if err != nil {
		err = json.Unmarshal(response, &objarray)
		if err != nil {
			sendError(w, http.StatusBadGateway, "The action did not return a dictionary or array.")
			return
		}
	}

	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Content-Length", fmt.Sprintf("%d", len(response)))
	numBytesWritten, err := w.Write(response)

	// flush output
	if f, ok := w.(http.Flusher); ok {
		f.Flush()
	}

	// diagnostic when you have writing problems
	if err != nil {
		sendError(w, http.StatusInternalServerError, fmt.Sprintf("Error writing response: %v", err))
		return
	}
	if numBytesWritten != len(response) {
		sendError(w, http.StatusInternalServerError, fmt.Sprintf("Only wrote %d of %d bytes to response", numBytesWritten, len(response)))
		return
	}
}