Docker pull源码分析报告
(一)Docker架构概述
Docker采用了典型的C/S架构,由Docker Client和Docker Daemon组成。其中Daemon分为Server和Engine两大部分,Server用于接收Client发送过来的请求,并经由Route路由至相应的Handler中,再通过Engine管理该请求对应的Docker容器。Docker架构如下图所示。
(二)Docker pull源码分析
本文所有源码分析均基于docker的最新版本,即v20.10.10
- 第一步,先找到整个Docker daemon的main函数入口,经梳理,发现位于moby/cmd/dockerd/docker.go目录下(注:moby为现在Docker项目的项目名称)
func main() {
if reexec.Init() {
return
}
//初始化日志的格式,在加载daemon配置文件后更新
logrus.SetFormatter(&logrus.TextFormatter{
TimestampFormat: jsonmessage.RFC3339NanoFixed,
FullTimestamp: true,
})
//配置终端相关的信息
_, stdout, stderr := term.StdStreams()
initLogging(stdout, stderr)
onError := func(err error) {
fmt.Fprintf(stderr, "%s\n", err)
os.Exit(1)
}
cmd, err := newDaemonCommand() //创建Daemon的cmd
if err != nil {
onError(err)
}
cmd.SetOut(stdout)
if err := cmd.Execute(); err != nil { //执行该cmd
onError(err)
}
}
- 可以发现,在执行cmd.Execute()语句之后,触发了daemon的执行,于是顺着该函数入口继续探索,会发现cmd.Execute()只是对ExecuteC这个函数的二次封装
func (c *Command) Execute() error { //实际执行ExecuteC()
_, err := c.ExecuteC()
return err
}
- 继续往下,查看ExecuteC()的源码
func (c *Command) ExecuteC() (cmd *Command, err error) {
if c.ctx == nil { //检查c是否有上下文
c.ctx = context.Background()
}
// 保证command只在root上运行
if c.HasParent() {
return c.Root().ExecuteC()
}
//windows hook
if preExecHookFn != nil {
preExecHookFn(c)
}
//初始化帮助命令 Docker help xxx
c.InitDefaultHelpCmd()
args := c.args
if c.args == nil && filepath.Base(os.Args[0]) != "cobra.test" {
args = os.Args[1:]
}
// 初始化全部的命令
c.initCompleteCmd(args)
var flags []string
if c.TraverseChildren { //在执行子命令前解析全部的有关父命令的flags
cmd, flags, err = c.Traverse(args)
} else {
cmd, flags, err = c.Find(args)
}
if err != nil { //出错处理
if cmd != nil {
c = cmd
}
if !c.SilenceErrors {
c.PrintErrln("Error:", err.Error())
c.PrintErrf("Run '%v --help' for usage.\n", c.CommandPath())
}
return c, err
}
cmd.commandCalledAs.called = true
if cmd.commandCalledAs.name == "" {
cmd.commandCalledAs.name = cmd.Name()
}
// 这边的代码需要保证将全部的上下文环境传递给子命令
// 如果父命令存在上下文
if cmd.ctx == nil {
cmd.ctx = c.ctx
}
err = cmd.execute(flags) //启动cmd
if err != nil {
if err == flag.ErrHelp {
cmd.HelpFunc()(cmd, args)
return cmd, nil
}
if !cmd.SilenceErrors && !c.SilenceErrors {
c.PrintErrln("Error:", err.Error())
}
if !cmd.SilenceUsage && !c.SilenceUsage {
c.Println(cmd.UsageString())
}
}
return cmd, err
}
- 至此,梳理了docker启动的思路,即在docker.go中配置cmd环境并启动。上述代码粗略的展现了有关cmd代码的执行路线。下面具体探究该过程。
- 第二步,细化过程。注意到在docker.go的main函数中,有一行代码如下:
cmd, err := newDaemonCommand() //创建Daemon的cmd
这行代码通过调用newDaemonCommand生成需要的cmd,供cmd执行execute执行(即前述过程),深入探究newDaemonCommand()的源代码如下:
func newDaemonCommand() (*cobra.Command, error) {
opts := newDaemonOptions(config.New()) //配置cmd所需参数
cmd := &cobra.Command{
Use: "dockerd [OPTIONS]",
Short: "A self-sufficient runtime for containers.",
SilenceUsage: true,
SilenceErrors: true,
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
opts.flags = cmd.Flags()
return runDaemon(opts) //启动Daemon
},
DisableFlagsInUseLine: true,
Version: fmt.Sprintf("%s, build %s", dockerversion.Version, dockerversion.GitCommit),
}
cli.SetupRootCommand(cmd)
flags := cmd.Flags() //获取和cmd相关的flags
flags.BoolP("version", "v", false, "Print version information and quit")
defaultDaemonConfigFile, err := getDefaultDaemonConfigFile()
if err != nil {
return nil, err
}
flags.StringVar(&opts.configFile, "config-file", defaultDaemonConfigFile, "Daemon configuration file")
opts.InstallFlags(flags)
if err := installConfigFlags(opts.daemonConfig, flags); err != nil {
return nil, err
}
installServiceFlags(flags)
return cmd, nil
}
- 注意到关键的执行函数为runDaemon(),于是继续探究这个函数。
func runDaemon(opts *daemonOptions) error {
daemonCli := NewDaemonCli() //创建一个daemoncli
stop, runAsService, err := initService(daemonCli) //在windows上可能作为service启动,初始化daemonCli
if err != nil {
logrus.Fatal(err)
}
if stop {
return nil
}
//针对windows的设置
if opts.configFile == "" {
opts.configFile = filepath.Join(opts.daemonConfig.Root, `config\daemon.json`)
}
if runAsService {
opts.daemonConfig.Pidfile = ""
} else if opts.daemonConfig.Pidfile == "" {
opts.daemonConfig.Pidfile = filepath.Join(opts.daemonConfig.Root, "docker.pid")
}
err = daemonCli.start(opts) //调用start函数启动cli
notifyShutdown(err)
return err
}
- runDaemon函数继续进行了配置,并且针对windows系统进行了特殊配置,最终调用start函数启动。start()函数中载入了相关的配置文件(对daemon,api server等的配置)。其中关于调用了initRouter()函数用于初始化路由信息,并且设置启用或者禁用等。代码如下:
func initRouter(opts routerOptions) {
decoder := runconfig.ContainerDecoder{
GetSysInfo: func() *sysinfo.SysInfo { //获取系统信息
return opts.daemon.RawSysInfo()
},
}
routers := []router.Router{ //初始化路由列表,对应多种docker client请求
//在容器路由器被删除之前添加checkpoint
checkpointrouter.NewRouter(opts.daemon, decoder),
container.NewRouter(opts.daemon, decoder, opts.daemon.RawSysInfo().CgroupUnified), //容器相关的路由
image.NewRouter(opts.daemon.ImageService()), //镜像相关的路由
systemrouter.NewRouter(opts.daemon, opts.cluster, opts.buildkit, opts.features),//系统相关路由
volume.NewRouter(opts.daemon.VolumesService()),//容器存储卷相关的路由
build.NewRouter(opts.buildBackend, opts.daemon, opts.features),
sessionrouter.NewRouter(opts.sessionManager),
swarmrouter.NewRouter(opts.cluster),
pluginrouter.NewRouter(opts.daemon.PluginManager()),
distributionrouter.NewRouter(opts.daemon.ImageService()),
}
grpcBackends := []grpcrouter.Backend{}
for _, b := range []interface{}{opts.daemon, opts.buildBackend} {
if b, ok := b.(grpcrouter.Backend); ok {
grpcBackends = append(grpcBackends, b)
}
}
if len(grpcBackends) > 0 {
routers = append(routers, grpcrouter.NewRouter(grpcBackends...))
}
if opts.daemon.NetworkControllerEnabled() {
routers = append(routers, network.NewRouter(opts.daemon, opts.cluster))
}
if opts.daemon.HasExperimental() { //Experimental定义启用或者是禁用的路由
for _, r := range routers {
for _, route := range r.Routes() {
if experimental, ok := route.(router.ExperimentalRoute); ok {
experimental.Enable()
}
}
}
}
opts.api.InitRouter(routers...)
}
- 将重点聚焦在NewRouter()函数中,每种类型会对应一个属于自己的NewRouter(),这里围绕image类型的router继续探究。
// NewRouter初始化一个新的image路由
func NewRouter(backend Backend) router.Router {
r := &imageRouter{backend: backend} //初始化对应的后端backend
r.initRoutes() //调用initRoutes()初始化路由
return r
}
- 继续探索initRoutes函数()
// initRoutes初始化image路由器中的路由
func (r *imageRouter) initRoutes() {
r.routes = []router.Route{
// GET
router.NewGetRoute("/images/json", r.getImagesJSON),
router.NewGetRoute("/images/search", r.getImagesSearch),
router.NewGetRoute("/images/get", r.getImagesGet),
router.NewGetRoute("/images/{name:.*}/get", r.getImagesGet),
router.NewGetRoute("/images/{name:.*}/history", r.getImagesHistory),
router.NewGetRoute("/images/{name:.*}/json", r.getImagesByName),
// POST
router.NewPostRoute("/images/load", r.postImagesLoad),
router.NewPostRoute("/images/create", r.postImagesCreate),//pull镜像相关的路由信息
router.NewPostRoute("/images/{name:.*}/push", r.postImagesPush), //push镜像相关的路由信息
router.NewPostRoute("/images/{name:.*}/tag", r.postImagesTag),
router.NewPostRoute("/images/prune", r.postImagesPrune),
// DELETE
router.NewDeleteRoute("/images/{name:.*}", r.deleteImages),
}
}
- 继续探索r.postImageCreate()函数
// 通过pull或import获取(创建)镜像,这是一个handler
func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil { //出错处理
return err
}
var (
image = r.Form.Get("fromImage")
repo = r.Form.Get("repo")
tag = r.Form.Get("tag")
message = r.Form.Get("message")
err error
output = ioutils.NewWriteFlusher(w)
platform *specs.Platform
)
defer output.Close() //最后关闭输出
w.Header().Set("Content-Type", "application/json")
version := httputils.VersionFromContext(ctx) //获取版本信息
if versions.GreaterThanOrEqualTo(version, "1.32") { //大于等于1.32的版本 对platform赋值
apiPlatform := r.FormValue("platform")
if apiPlatform != "" {
sp, err := platforms.Parse(apiPlatform)
if err != nil {
return err
}
platform = &sp
}
}
if image != "" { //拉取镜像
metaHeaders := map[string][]string{}
for k, v := range r.Header {
if strings.HasPrefix(k, "X-Meta-") {
metaHeaders[k] = v
}
}
authEncoded := r.Header.Get("X-Registry-Auth")
authConfig := &types.AuthConfig{}
if authEncoded != "" {
authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
// pull 可以不需要身份验证
// 默认为空,保证兼容性
authConfig = &types.AuthConfig{}
}
}
err = s.backend.PullImage(ctx, image, tag, platform, metaHeaders, authConfig, output)
} else { //导入镜像
src := r.Form.Get("fromSrc")
os := ""
if platform != nil {
os = platform.OS
}
err = s.backend.ImportImage(src, repo, os, tag, message, r.Body, output, r.Form["changes"])
}
if err != nil {
if !output.Flushed() {
return err
}
_, _ = output.Write(streamformatter.FormatError(err))
}
return nil
}
- 至此,router通过backend调用的PullImage和ImportImage进行具体的操作。在往下看,可以发现一个Interface。
type registryBackend interface {
PullImage(ctx context.Context, image, tag string, platform *specs.Platform, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error //关于拉取镜像的函数
PushImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error //关于推送镜像的函数
SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, limit int, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error) //在仓库中寻找镜像
}
- 可以发现,在moby\daemon\images中定义了ImageService类实现了registryBackend接口中定义的函数。
- PullImage源代码:
// PullImage启动镜像的拉取操作。 image指的是仓库名称, tag可以为空, 也可以指定
func (i *ImageService) PullImage(ctx context.Context, image, tag string, platform *specs.Platform, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
start := time.Now() //记录当前的时间戳,用于统计镜像拉取的时间
// 特殊情况: "pull -a" ,拉取所有tag的镜像。这个命令里面可能末尾有冒号,特殊处理一下。
image = strings.TrimSuffix(image, ":")
ref, err := reference.ParseNormalizedNamed(image) //进行image name检查
if err != nil {
return errdefs.InvalidParameter(err) //无效的参数
}
if tag != "" {
var dgst digest.Digest
dgst, err = digest.Parse(tag) //tag可以不全
if err == nil {
ref, err = reference.WithDigest(reference.TrimNamed(ref), dgst)
} else {
ref, err = reference.WithTag(ref, tag)
}
if err != nil {
return errdefs.InvalidParameter(err)
}
}
err = i.pullImageWithReference(ctx, ref, platform, metaHeaders, authConfig, outStream) //拉取镜像
imageActions.WithValues("pull").UpdateSince(start)
if err != nil {
return err
}
if platform != nil {
//如果指定了--platform,需要进行平台类型检查
img, err := i.GetImage(image, platform)
//获取image的特殊情况:https://github.com/docker/docker/blob/v20.10.7/daemon/images/image.go#L175-L183
if errdefs.IsNotFound(err) && img != nil {
po := streamformatter.NewJSONProgressOutput(outStream, false)
progress.Messagef(po, "", `WARNING: %s`, err.Error())
logrus.WithError(err).WithField("image", image).Warn("ignoring platform mismatch on single-arch image")
}
}
return nil
}
- PushImage源代码
//推送镜像
func (i *ImageService) PushImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
start := time.Now() //记录时间戳
ref, err := reference.ParseNormalizedNamed(image) //检查name
if err != nil {
return err
}
if tag != "" {
// 只能tag,不可以省略
ref, err = reference.WithTag(ref, tag)
if err != nil {
return err
}
}
// 包括一个缓冲区,用于保障当client很慢的时候的性能
progressChan := make(chan progress.Progress, 100)
writesDone := make(chan struct{})
ctx, cancelFunc := context.WithCancel(ctx)
go func() {
progressutils.WriteDistributionProgress(cancelFunc, outStream, progressChan)
close(writesDone)
}()
imagePushConfig := &distribution.ImagePushConfig{
Config: distribution.Config{
MetaHeaders: metaHeaders,
AuthConfig: authConfig,
ProgressOutput: progress.ChanOutput(progressChan),
RegistryService: i.registryService,
ImageEventLogger: i.LogImageEvent,
MetadataStore: i.distributionMetadataStore,
ImageStore: distribution.NewImageConfigStoreFromStore(i.imageStore),
ReferenceStore: i.referenceStore,
},
ConfigMediaType: schema2.MediaTypeImageConfig,
LayerStores: distribution.NewLayerProvidersFromStore(i.layerStore),
TrustKey: i.trustKey,
UploadManager: i.uploadManager,
}
err = distribution.Push(ctx, ref, imagePushConfig) //push镜像的操作
close(progressChan)
<-writesDone
imageActions.WithValues("push").UpdateSince(start)
return err
}
- SearchRegistryForImages源代码
//SearchRegistryForImages用于查询登录的用户仓库中(根据authConfig)对应的image
func (i *ImageService) SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, limit int,
authConfig *types.AuthConfig,
headers map[string][]string) (*registrytypes.SearchResults, error) {
searchFilters, err := filters.FromJSON(filtersArgs)
if err != nil {
return nil, err
}
if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
return nil, err
}
var isAutomated, isOfficial bool
var hasStarFilter = 0
if searchFilters.Contains("is-automated") {
if searchFilters.UniqueExactMatch("is-automated", "true") {
isAutomated = true
} else if !searchFilters.UniqueExactMatch("is-automated", "false") {
return nil, invalidFilter{"is-automated", searchFilters.Get("is-automated")}
}
}
if searchFilters.Contains("is-official") {
if searchFilters.UniqueExactMatch("is-official", "true") {
isOfficial = true
} else if !searchFilters.UniqueExactMatch("is-official", "false") {
return nil, invalidFilter{"is-official", searchFilters.Get("is-official")}
}
}
if searchFilters.Contains("stars") {
hasStars := searchFilters.Get("stars")
for _, hasStar := range hasStars {
iHasStar, err := strconv.Atoi(hasStar)
if err != nil {
return nil, invalidFilter{"stars", hasStar}
}
if iHasStar > hasStarFilter {
hasStarFilter = iHasStar
}
}
}
unfilteredResult, err := i.registryService.Search(ctx, term, limit, authConfig, dockerversion.DockerUserAgent(ctx), headers)
if err != nil {
return nil, err
}
filteredResults := []registrytypes.SearchResult{}
for _, result := range unfilteredResult.Results {
if searchFilters.Contains("is-automated") {
if isAutomated != result.IsAutomated {
continue
}
}
if searchFilters.Contains("is-official") {
if isOfficial != result.IsOfficial {
continue
}
}
if searchFilters.Contains("stars") {
if result.StarCount < hasStarFilter {
continue
}
}
filteredResults = append(filteredResults, result)
}
return ®istrytypes.SearchResults{
Query: unfilteredResult.Query,
NumResults: len(filteredResults),
Results: filteredResults,
}, nil
}
结语
本文研究探索了Docker pull命令从发起到执行的过程。总结如下:
- 在终端输入docker pull xxx命令后,Docker Client捕获到该命令,将其以http请求的方式发送给Docker Daemon中的Docker Server(请求URL一般为"/images/create?"+“xxx”)
- Docker Daemon中有Routers(路由)对象,会根据请求的URL路由到对应的处理模块,比如pull会路由到postImagesCreate()函数中
- 最后通过ImageService类实现registryBackend接口中定义的函数处理相应的拉取镜像逻辑