真·深入底层原理
本文章以openwhisk平台为例
首先,当客户端的请求,经过平台组件的层层处理和传递(这个过程时间很快),到达openwhisk的invoker组件,它会调用docker对容器进行比较直接的管理。
首先,根据用户上传的action的语言,平台会选择相匹配的语言runtime(运行时,即为某一个语言对应的各种依赖,且这个runtime会被打包成容器镜像,供以启动容器),invoker会调用docker,使用docker run来启动容器。
那容器是怎么对我们上传的源码进行处理,将其运行,并最终将运行结果返回给我们的呢。
以go语言为例
go语言的runtime,也就是对应的镜像,其docker file内容如下
#
# 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
}
}