Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1366506
  • 博文数量: 488
  • 博客积分: 161
  • 博客等级: 入伍新兵
  • 技术积分: 5064
  • 用 户 组: 普通用户
  • 注册时间: 2011-07-01 07:37
个人简介

只有偏执狂才能生存

文章分类

全部博文(488)

文章存档

2016年(10)

2015年(112)

2014年(66)

2013年(272)

2012年(28)

分类: LINUX

2015-08-10 10:48:04

这里分析下docker的实现,由于源码较多,所以会分几篇文章来分析,这里是第一篇。
入口代码为docker/docker.go。我们从main开始看起吧,关于docker的Makefile文件的结构可以参考小秦之前的文章:

1
2
3
4
func main() {
    ifreexec.Init() {
        return
    } 

这里reexec是docker自己实现的一个package,作用的话根据其README文件说实话没看懂。。。不过根据代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Register adds an initialization func under the specified name
func Register(name string, initializer func()) {
    if_, exists := registeredInitializers[name]; exists {
        panic(fmt.Sprintf("reexec func already registred under name %q", name))
    }  
 
    registeredInitializers[name] = initializer
}
 
// Init is called as the first part of the exec process and returns true if an
// initialization function was called.
func Init() bool{
    initializer, exists := registeredInitializers[os.Args[0]]
    ifexists {
        initializer()
 
        returntrue
    }  
    returnfalse
}

猜测应该是有一些函数会调用Register注册一些初始化方法,然后在docker的main这里通过Init方法就能执行这些注册了的初始化方法。不过感觉这里用的怪怪的,因为go毕竟不是python,main的开头并没有别的代码那么哪里来的调用Register的地方呢?https://groups.google.com/forum/#!topic/docker-dev/ePLDji_qBvE这里给的解释是这里可能是个过期的代码。所以我们就不去深入了。更正:go的init方法会在main方法前调用,所以在源代码里其实散落了很多init的函数调用了Register。

接着看代码:

1
initLogging(stderr)

其实现为:

1
2
3
func initLogging(stderr io.Writer) {
    logrus.SetOutput(stderr)

logrus是一个第三方的log模块,文档可以在https://github.com/Sirupsen/logrus中查看。由于用法很简单所以我们就不看了,这里initLogging做的事情太简单了,就是将我们的输出定向到stderr。

继续看代码:

1
2
flag.Parse()
// FIXME: validate daemon flags here

flag模块是用于处理我们的命令行参数的。有兴趣的可以深入了解下:

1
2
3
4
5
6
7
8
9
10
11
// Parse parses the command-line flags from os.Args[1:].  Must be called
// after all flags are defined and before flags are accessed by the program.
func Parse() {
    // Ignore errors; CommandLine is set for ExitOnError.
    CommandLine.Parse(os.Args[1:])
}
......
// CommandLine is the default set of command-line flags, parsed from os.Args.
// The top-level functions such as BoolVar, Arg, and on are wrappers for the
// methods of CommandLine.
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

继续看我们的main的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if*flVersion {
    showVersion()
    return 
}
 
if*flConfigDir != ""{
    cliconfig.SetConfigDir(*flConfigDir)
}
 
if*flLogLevel != ""{
    lvl, err := logrus.ParseLevel(*flLogLevel)
    iferr != nil {
        fmt.Fprintf(os.Stderr, "Unable to parse logging level: %s\n", *flLogLevel)
        os.Exit(1)
    }
    setLogLevel(lvl)
} else{
    setLogLevel(logrus.InfoLevel)
}
 
if*flDebug {
    os.Setenv("DEBUG", "1")
    setLogLevel(logrus.DebugLevel)
}

上面的flag模块会根据命令行提供的参数为这里的flVersion等变量赋值,这里就是根据参数调用对应的函数了。这里的几个看字面意思应该就能清楚其含义。我们继续看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
iflen(flHosts) == 0 {
    defaultHost := os.Getenv("DOCKER_HOST")
    ifdefaultHost == ""|| *flDaemon {
        ifruntime.GOOS != "windows"{
            // If we do not have a host, default to unix socket
            defaultHost = fmt.Sprintf("unix://%s", opts.DefaultUnixSocket)
        } else{
            // If we do not have a host, default to TCP socket on Windows
            defaultHost = fmt.Sprintf("tcp://%s:%d", opts.DefaultHTTPHost, opts.DefaultHTTPPort)
        }
    }
    defaultHost, err := opts.ValidateHost(defaultHost)
    iferr != nil {
        if*flDaemon {
            logrus.Fatal(err)
        } else{
            fmt.Fprint(os.Stderr, err)
        }
        os.Exit(1)
    }
    flHosts = append(flHosts, defaultHost)
}

如果没有指定host参数,则对于windows会使用默认的tcp连接,而对于非windows目前都是使用本地unix套接字。继续看代码,类似上面这种简单的我们就不看了:

1
2
3
4
5
6
7
8
9
10
if*flDaemon {
    if*flHelp {
        flag.Usage()
        return
    }
    mainDaemon()
    return
}
 
// From here on, we assume we're a client, not a server.

如果是daemon,则会的执行mainDaemon然后直接返回。我们知道daemon就是我们的docker后台守护服务,docker的命令大部分都是发给这个daemon的。mainDaemon我们稍后再看,先把这里的main看完。接着的代码中重要的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
cli := client.NewDockerCli(stdin, stdout, stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], tlsConfig)
 
iferr := cli.Cmd(flag.Args()...); err != nil {
    ifsterr, ok := err.(client.StatusError); ok {
        ifsterr.Status != ""{
            fmt.Fprintln(cli.Err(), sterr.Status)
            os.Exit(1)
        }
        os.Exit(sterr.StatusCode)
    }
    fmt.Fprintln(cli.Err(), err)
    os.Exit(1)
}

lag.Args()拥有我们的所有参数,所以这里的关键是我们的client.NewDockerCli。来看下其实现的重要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// The transport is created here for reuse during the client session.
tr := &http.Transport{
    TLSClientConfig: tlsConfig,
}
sockets.ConfigureTCPTransport(tr, proto, addr)
 
configFile, e := cliconfig.Load(cliconfig.ConfigDir())
ife != nil {
    fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e)
}
 
return&DockerCli{
    proto:         proto,
    addr:          addr,
    configFile:    configFile,
    in:            in,
    out:           out,
    err:           err,
    keyFile:       keyFile,
    inFd:          inFd,
    outFd:         outFd,
    isTerminalIn:  isTerminalIn,
    isTerminalOut: isTerminalOut,
    tlsConfig:     tlsConfig,
    scheme:        scheme,
    transport:     tr,
}

从代码里可以看到这里我们建立了一个http的Transport,然后传递给了DockerCli,实际的活还是由DockerCli来完成。看下DockerCli的Cmd的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Cmd executes the specified command.
func (cli *DockerCli) Cmd(args ...string) error {
    iflen(args) > 1 {
        method, exists := cli.getMethod(args[:2]...)
        ifexists {
            returnmethod(args[2:]...)
        }
    }
    iflen(args) > 0 {
        method, exists := cli.getMethod(args[0])
        if!exists {
            returnfmt.Errorf("docker: '%s' is not a docker command.\nSee 'docker --help'.", args[0])
        }
        returnmethod(args[1:]...)
    }
    returncli.CmdHelp()
}
......
func (cli *DockerCli) getMethod(args ...string) (func(...string) error, bool) {
    camelArgs := make([]string, len(args))
    fori, s := range args {
        iflen(s) == 0 {
            returnnil, false
        }
        camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
    }
    methodName := "Cmd"+ strings.Join(camelArgs, "")
    method := reflect.ValueOf(cli).MethodByName(methodName)
    if!method.IsValid() {
        returnnil, false
    }
    returnmethod.Interface().(func(...string) error), true
}

=-=看到go的这个代码表示有点晕啊。首先是根据传递的参数做了些拼接等操作后获取一个method的名字,然后通过反射从cli处获取真实的method。在client的目录下我们可以看到这些函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
[root@dev client]# grep Cmd ./* | grep func | cut -f4 -d' '| cut -f1 -d'('| sort | uniq
Cmd
CmdAttach
CmdBuild
CmdCommit
CmdCp
CmdCreate
CmdDiff
CmdEvents
CmdExec
CmdExport
CmdHelp
CmdHistory
CmdImages
CmdImport
CmdInfo
CmdInspect
CmdKill
CmdLoad
CmdLogin
CmdLogout
CmdLogs
CmdNetwork
CmdPause
CmdPort
CmdPs
CmdPull
CmdPush
CmdRename
CmdRestart
CmdRm
CmdRmi
CmdRun
CmdSave
CmdSearch
CmdService
CmdStart
CmdStats
CmdStop
CmdTag
CmdTop
CmdUnpause
CmdVersion
CmdWait

我们看下CmdPs的实现,重要代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
rdr, _, _, err := cli.call("GET", "/containers/json?"+v.Encode(), nil, nil)
iferr != nil {
    returnerr
}
 
defer rdr.Close()
 
containers := []types.Container{}
iferr := json.NewDecoder(rdr).Decode(&containers); err != nil {
    returnerr
}
 
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
if!*quiet {
    fmt.Fprint(w, "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
 
    if*size {
        fmt.Fprintln(w, "\tSIZE")
    } else{
        fmt.Fprint(w, "\n")
    }
}

可以看到CmdPs(或者说其它的大部分命令)的实现都是通过调用一个HTTP请求给daemon来实现的。cli的call的实现不难猜测和我们上面说的transport有关,代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func (cli *DockerCli) call(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) {
    params, err := cli.encodeData(data)
    iferr != nil {
        returnnil, nil, -1, err
    }  
 
    ifdata != nil {
        ifheaders == nil {
            headers = make(map[string][]string)
        }
        headers["Content-Type"] = []string{"application/json"}
    }  
 
    serverResp, err := cli.clientRequest(method, path, params, headers)
    returnserverResp.body, serverResp.header, serverResp.statusCode, err
}
 
......
 
func (cli *DockerCli) clientRequest(method, path string, in io.Reader, headers map[string][]string) (*serverResponse, error) {
 
    serverResp := &serverResponse{
        body:       nil,
        statusCode: -1,
    }
    ......
    req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION+" ("+runtime.GOOS+")")
    req.URL.Host = cli.addr
    req.URL.Scheme = cli.scheme
    ......
    ifexpectedPayload && req.Header.Get("Content-Type") == ""{
        req.Header.Set("Content-Type", "text/plain")
    }
 
    resp, err := cli.HTTPClient().Do(req)
    ......
 
......
 
// HTTPClient creates a new HTTP client with the cli's client transport instance.
func (cli *DockerCli) HTTPClient() *http.Client {
    return&http.Client{Transport: cli.transport}
}

所以我们可以认为对于docker的client的命令,会通过上面的流程发送一个HTTP请求给daemon获取具体的结果。

现在我们来看daemon,上面看到daemon的入口是mainDaemon。代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func mainDaemon() {
    ifutils.ExperimentalBuild() {
        logrus.Warn("Running experimental build")
    }  
 
    ifflag.NArg() != 0 {
        flag.Usage()
        return
    }  
 
    logrus.SetFormatter(&logrus.TextFormatter{TimestampFormat: timeutils.RFC3339NanoFixed})
 
    iferr := setDefaultUmask(); err != nil {
        logrus.Fatalf("Failed to set umask: %v", err)
    } 

这里的代码没有什么特别的,注意这里log的格式设置了下时间格式。umask就是linux的umask,默认设置为了0022。接着看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
var pfile *pidfile.PidFile
ifdaemonCfg.Pidfile != ""{
    pf, err := pidfile.New(daemonCfg.Pidfile)
    iferr != nil {
        logrus.Fatalf("Error starting daemon: %v", err)
    }
    pfile = pf
    defer func() {
        iferr := pfile.Remove(); err != nil {
            logrus.Error(err)
        }
    }()
}

这里设置了pid文件。defer表示在daemon退出的时候会尝试移除这个文件。go中可以通过os.Getpid()获取当前进程的pid。继续看代码:

1
2
3
4
5
6
7
8
9
serverConfig := &apiserver.ServerConfig{
    Logging:     true,
    EnableCors:  daemonCfg.EnableCors,
    CorsHeaders: daemonCfg.CorsHeaders,
    Version:     dockerversion.VERSION,
}
serverConfig = setPlatformServerConfig(serverConfig, daemonCfg)
......
api := apiserver.New(serverConfig)

这里初始化了一个apiserver.ServerConfig结构体作为我们api服务器的配置文件,然后的代码通过New生成了一个apiserver。至于apiserver的实现我们会再以后的文章中分析。先继续看代码:

1
2
3
4
5
6
7
8
9
10
11
12
// The serve API routine never exits unless an error occurs
// We need to start it as a goroutine and wait on it so
// daemon doesn't exit
serveAPIWait := make(chan error)
go func() {
    iferr := api.ServeApi(flHosts); err != nil {
        logrus.Errorf("ServeAPI error: %v", err)
        serveAPIWait <- err
        return
    }
    serveAPIWait <- nil
}()

从go的实现上我们可以知道接下来的某个地方肯定有代码在等待serveAPIWait获取消息。所以可以认为此时我们的apiserver已经运行起来了。接着看代码:

1
2
3
4
5
6
7
8
9
10
registryService := registry.NewService(registryCfg)
d, err := daemon.NewDaemon(daemonCfg, registryService)
iferr != nil {
    ifpfile != nil {
        iferr := pfile.Remove(); err != nil {
            logrus.Error(err)
        }
    }
    logrus.Fatalf("Error starting daemon: %v", err)
}

关于daemon我们也在之后的文章中再去了解其实现细节。现在咋mainDaemon中还剩余的代码只有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
signal.Trap(func() {
    api.Close()
    <-serveAPIWait
    shutdownDaemon(d, 15)
    ifpfile != nil {
        iferr := pfile.Remove(); err != nil {
            logrus.Error(err)
        }
    }
})
 
// after the daemon is done setting up we can tell the api to start
// accepting connections with specified daemon
api.AcceptConnections(d)
 
// Daemon is fully initialized and handling API traffic
// Wait for serve API to complete
errAPI := <-serveAPIWait
shutdownDaemon(d, 15)
iferrAPI != nil {
    ifpfile != nil {
        iferr := pfile.Remove(); err != nil {
            logrus.Error(err)
        }
    }
    logrus.Fatalf("Shutting down due to ServeAPI error: %v", errAPI)
}

从注释可以看到这里的代码的核心是api.AcceptConnections(d),此时我们的daemon就提供api服务了。另外可以注意到d是apiserver的参数,或许api的后端通过d来实现?不管怎样,mainDaemon的实现我们算是简单过了一遍,其主要就是启动了一个api server,并且给了api server一个daemon参数。

总结下吧:
1. docker的client和daemon都是同一个文件提供的,源码的入口在docker/docker.go中
2. client的命令会从参数转换成CmdXXX的形式,通过反射的方法可以在client目录下找到每个命令的对应实现。实现基本上是发送一个HTTP请求给daemon获取结果
3. daemon的启动会启动一个api server,并且会启动一个daemon service。api server应该是起路由的作用,而daemon service应该是真正干活的。具体的我们下面的文章再分析。

阅读(884) | 评论(0) | 转发(0) |
0

上一篇:Docker源码学习

下一篇:Docker源码学习2

给主人留下些什么吧!~~
评论热议
请登录后评论。

登录 注册