Docker pull源码分析

Docker pull源码分析报告

(一)Docker架构概述

Docker采用了典型的C/S架构,由Docker Client和Docker Daemon组成。其中Daemon分为Server和Engine两大部分,Server用于接收Client发送过来的请求,并经由Route路由至相应的Handler中,再通过Engine管理该请求对应的Docker容器。Docker架构如下图所示。

(img-K72tRDGW-1636125539718)(C:\Users\12610\AppData\Roaming\Typora\typora-user-images\image-20211105115726747.png)]

(二)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 &registrytypes.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接口中定义的函数处理相应的拉取镜像逻辑
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值