distribution structure and start 分析

distribution structure and start 分析

本文簡單介紹一下distribution的結構以及其啓動過程。
這裏分析的對象不包括存儲驅動框架,hook框架以及認證體系。
本文應該是distribution系列文章中最早的一篇,但是由於各種原因其書寫時間晚於pull push過程中distribution處理源碼分析之後,因此新的架構圖中有些是基於完全看完代碼後纔有的內容,這裏大家先看着。

簡介
distribution是docker體系的鏡像倉庫——即docker registry。 docker registry有兩個版本,第一個是python寫的 registry, 在docker1.6之前支持;第二個版本則是使用go語言寫的distribution。
本文結合distribution的運行命令執行的過程監督分析一下distribution的框架與啓動流程。

我們先看一下distribution的架構圖:


這裏寫圖片描述

前面我們展示了distribution的架構圖,從該圖可以明確看到其實distribution就是一個能提供特殊文件(Manifest,Blob,Tag,Catalog<這幾個也是相互關聯的>)存儲的http服務,因此其啓動過程主要是根據配置參數創建後端存儲服務以及對外服務的http server的過程。
下面我們來看一下distribution的Start UP過程。

我們的分析過程中忽略Configuration的解析,這裏統一以本地存儲的filesystem爲例做簡單分析。

總覽

我們先來看一下distribution的main函數:

    func main() {
        registry.RootCmd.Execute()
    }

該Execute函數是spf13/cobra包的Command對應的Execute, 該函數會調用對應cmd的excute直至調用對應cmd的Run函數,具體的調用與分析流程這裏不做詳細描述,我們直接進入distribution的cmd——ServeCmd 來看。
運行distribution的時候輸入的參數是./registry serve 參考個命令字確定該命令對應的是ServeCmd.

ServeCmd的定義如下:

    var ServeCmd = &cobra.Command{
        Use:   "serve <config>",
        Short: "`serve` stores and distributes Docker images",
        Long:  "`serve` stores and distributes Docker images.",
        Run: func(cmd *cobra.Command, args []string) {

            // setup context
            ctx := context.WithVersion(context.Background(), version.Version)

            config, err := resolveConfiguration(args)
            if err != nil {
                fmt.Fprintf(os.Stderr, "configuration error: %v\n", err)
                cmd.Usage()
                os.Exit(1)
            }

            if config.HTTP.Debug.Addr != "" {
                go func(addr string) {
                    log.Infof("debug server listening %v", addr)
                    if err := http.ListenAndServe(addr, nil); err != nil {
                        log.Fatalf("error listening on debug interface: %v", err)
                    }
                }(config.HTTP.Debug.Addr)
            }

            registry, err := NewRegistry(ctx, config)
            if err != nil {
                log.Fatalln(err)
            }

            if err = registry.ListenAndServe(); err != nil {
                log.Fatalln(err)
            }
        },
    }  

由此進入Run函數, 該函數非常簡單命令,首先創建一個Contex結構,接下來解析配置文件, 這兩部分我們不做分析,有興趣的自己去分析。
接下來纔是該函數的核心內容,調用NewRegistry創建registry,這是這個函數的核心。創建了registry之後調用registry的ListenAndServe()函數開始對我提供http服務,這個我們之前的http相關的文章中已經分析過了,本篇不做分析。

New Registry

接下來來看NewRegistry:

    // NewRegistry creates a new registry from a context and configuration struct.
    func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Registry, error) {
        var err error
        ctx, err = configureLogging(ctx, config)
        if err != nil {
            return nil, fmt.Errorf("error configuring logger: %v", err)
        }

        // inject a logger into the uuid library. warns us if there is a problem
        // with uuid generation under low entropy.
        uuid.Loggerf = context.GetLogger(ctx).Warnf

        app := handlers.NewApp(ctx, config)
        // TODO(aaronl): The global scope of the health checks means NewRegistry
        // can only be called once per process.
        app.RegisterHealthChecks()
        handler := configureReporting(app)
        handler = alive("/", handler)
        handler = health.Handler(handler)
        handler = panicHandler(handler)
        handler = gorhandlers.CombinedLoggingHandler(os.Stdout, handler)

        server := &http.Server{
            Handler: handler,
        }

        return &Registry{
            app:    app,
            config: config,
            server: server,
        }, nil
    }  

從上面的代碼來看主要是NewAPP,以及處理Handler,最後構建了Registry的結構,結合上面Run函數的分析以及distribution的總加工,這裏主要就是初始化生產APP,然後以APP來構建了以Http Server的handler。
那麼這最終需要的分析的對象就是:

  • handlers.NewApp(ctx, config)
  • app.RegisterHealthChecks()
  • handler := configureReporting(app)
  • handler = alive(“/”, handler)
  • handler = health.Handler(handler)
  • handler = panicHandler(handler)

這幾個中我們首先分析NewAPP,這個函數也是整個distribution啓動過程中最爲關鍵的。

NewAPP

這纔是整個流程中最關鍵的部分。
我們首先來看一下app的結構定義:

    type App struct {
        context.Context

        Config *configuration.Configuration

        router           *mux.Router                 // main application router, configured with dispatchers
        driver           storagedriver.StorageDriver // driver maintains the app global storage driver instance.
        registry         distribution.Namespace      // registry is the primary registry backend for the app instance.
        accessController auth.AccessController       // main access controller for application

        // httpHost is a parsed representation of the http.host parameter from
        // the configuration. Only the Scheme and Host fields are used.
        httpHost url.URL

        // events contains notification related configuration.
        events struct {
            sink   notifications.Sink
            source notifications.SourceRecord
        }

        redis *redis.Pool

        // trustKey is a deprecated key used to sign manifests converted to
        // schema1 for backward compatibility. It should not be used for any
        // other purposes.
        trustKey libtrust.PrivateKey

        // isCache is true if this registry is configured as a pull through cache
        isCache bool

        // readOnly is true if the registry is in a read-only maintenance mode
        readOnly bool
    }

從上面來看, 主要的結構對象有route、driver、registry、events個需要重點關注,其他的不能說不重要,只是相對好理解,但是並不需要太多的解析關注。
上面提到了events這個結構,這個結構主要是用來註冊各種hook信息的,很重要但是很抱歉,我沒有分析透徹,這裏先不做分析說明。
這樣我們下面的分析中重點針對的就是route,driver跟registry對象來進行分析了。
我們來看一下NewApp函數的內容吧:

    func NewApp(ctx context.Context, config *configuration.Configuration) *App {
        app := &App{
            Config:  config,
            Context: ctx,
            router:  v2.RouterWithPrefix(config.HTTP.Prefix),
            isCache: config.Proxy.RemoteURL != "",
        }

        // Register the handler dispatchers.
        app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler {
            return http.HandlerFunc(apiBase)
        })
        app.register(v2.RouteNameManifest, imageManifestDispatcher)
        app.register(v2.RouteNameCatalog, catalogDispatcher)
        app.register(v2.RouteNameTags, tagsDispatcher)
        app.register(v2.RouteNameBlob, blobDispatcher)
        app.register(v2.RouteNameBlobUpload, blobUploadDispatcher)
        app.register(v2.RouteNameBlobUploadChunk, blobUploadDispatcher)

        // override the storage driver's UA string for registry outbound HTTP requests
        storageParams := config.Storage.Parameters()

        ……

        var err error
        app.driver, err = factory.Create(config.Storage.Type(), storageParams)

        ……

        app.configureSecret(config)
        app.configureEvents(config)
        app.configureRedis(config)
        app.configureLogHook(config)

        ……

        if app.registry == nil {
            // configure the registry if no cache section is available.
            app.registry, err = storage.NewRegistry(app.Context, app.driver, options...)
            if err != nil {
                panic("could not create registry: " + err.Error())
            }
        }

        ……

        authType := config.Auth.Type()

        ……

        return app
    }

完整的NewApp代碼非常長,這裏將其中的一些option的配置信息刪除了,簡單的貼出了一些最關鍵最主要的邏輯部分。
接下來就一組一組的分析其初始化部分。
首先其定義了一個app的結構體,並對其中的部分進行了初始化。

route註冊

這裏的route註冊主要使用了app.register的函數來進行註冊的。
我們來看其中一個register的過程,以app.register(v2.RouteNameManifest, imageManifestDispatcher) 爲例:

app.router.GetRoute(routeName).Handler(app.dispatcher(dispatch))

這個函數僅僅只是網app.router.namedRoutes裏面添加指定的Handler。
具體這些RouteName的添加在router: v2.RouterWithPrefix(config.HTTP.Prefix)添加過了。

對應的route描述信息在 routeDescriptors = []RouteDescriptor{……}, 請大家仔細看這裏。

app.dispatcher(dispatch) 返回了一個新的hppt handler的方法,該方法是在dispatch方法前面添加了一些處理特殊option信息以及authoration信息處理。

        app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler {
            return http.HandlerFunc(apiBase)
        })
        app.register(v2.RouteNameManifest, imageManifestDispatcher)
        app.register(v2.RouteNameCatalog, catalogDispatcher)
        app.register(v2.RouteNameTags, tagsDispatcher)
        app.register(v2.RouteNameBlob, blobDispatcher)
        app.register(v2.RouteNameBlobUpload, blobUploadDispatcher)
        app.register(v2.RouteNameBlobUploadChunk, blobUploadDispatcher)

這段代碼描述該app僅僅提供 RouteNameBase、RouteNameManifest、RouteNameCatalog、RouteNameTags、RouteNameBlob、RouteNameBlobUpload和RouteNameBlobUploadChunk路徑的訪問服務,對應的路徑定義如下:

    const (
        RouteNameBase            = "base"
        RouteNameManifest        = "manifest"
        RouteNameTags            = "tags"
        RouteNameBlob            = "blob"
        RouteNameBlobUpload      = "blob-upload"
        RouteNameBlobUploadChunk = "blob-upload-chunk"
        RouteNameCatalog         = "catalog"
    )

每個路徑對應的請求的不通方法(Get、Put、Patch、Head、Delete等)的真正處理函數在對應的dispatch裏面定義的, 我們以RouteNameBlobUploadChunk爲例爲大家簡單show一下:

    func blobUploadDispatcher(ctx *Context, r *http.Request) http.Handler {
        buh := &blobUploadHandler{
            Context: ctx,
            UUID:    getUploadUUID(ctx),
        }

        handler := handlers.MethodHandler{
            "GET":  http.HandlerFunc(buh.GetUploadStatus),
            "HEAD": http.HandlerFunc(buh.GetUploadStatus),
        }

        if !ctx.readOnly {
            handler["POST"] = http.HandlerFunc(buh.StartBlobUpload)
            handler["PATCH"] = http.HandlerFunc(buh.PatchBlobData)
            handler["PUT"] = http.HandlerFunc(buh.PutBlobUploadComplete)
            handler["DELETE"] = http.HandlerFunc(buh.CancelBlobUpload)
        }

        if buh.UUID != "" {
            ……
        }

        return handler
    }

該函數中根據不通的方法指定了不通的http.Handler。

當上面的代碼執行完成後,route中僅僅也就RouteNameBase、RouteNameManifest、RouteNameTags、RouteNameBlob 、RouteNameBlobUpload、RouteNameBlobUploadChunk 和RouteNameCatalog七條路由,真正請求發來的時候,會跳轉到對應到對應的dispatch中再根據對應的方法找到對應的出路函數。

driver 初始化

我們直接看代碼吧:

    storageParams := config.Storage.Parameters()
    if storageParams == nil {
        storageParams = make(configuration.Parameters)
    }
    storageParams["useragent"] = fmt.Sprintf("docker-distribution/%s %s", version.Version, runtime.Version())

    var err error
    app.driver, err = factory.Create(config.Storage.Type(), storageParams)

核心代碼就兩行,第一行獲取storageParams, 這裏不做解析。
我們來看factory.Create(config.Storage.Type(), storageParams):

    func Create(name string, parameters map[string]interface{}) (storagedriver.StorageDriver, error) {
        driverFactory, ok := driverFactories[name]
        if !ok {
            return nil, InvalidStorageDriverError{name}
        }
        return driverFactory.Create(parameters)
    }

這裏的代碼中直接根據name 獲取driverFactory, 然後調用driverFactory.Create來創建storage的驅動。
name參數是我們啓動的時候在config.yml中配置的storage的項,會有 filesystem, swift, s3等可以指定。
driverFactory.Create是各自存儲驅動的Create函數,屬於存儲驅動層的東西,這裏也不做分析。

有人爲問道driverFactories[name] 啥時候初始化的,爲啥就能夠直接獲取到。 這裏涉及到Golang 的init()的調用機制,在main()對應的文件中import了所有的後端驅動包,每個驅動包的init()函數都會自動執行,在init()函數中註冊了各自的driverFactory。

我們用filesystem爲例來看一下:

onst (
    driverName           = "filesystem"
    defaultRootDirectory = "/var/lib/registry"
    defaultMaxThreads    = uint64(100)

    // minThreads is the minimum value for the maxthreads configuration
    // parameter. If the driver's parameters are less than this we set
    // the parameters to minThreads
    minThreads = uint64(25)
)

func init() {
    factory.Register(driverName, &filesystemDriverFactory{})
}

// filesystemDriverFactory implements the factory.StorageDriverFactory interface
type filesystemDriverFactory struct{}

這樣app.driver初始化成功。

app.registry 初始化

我們來看一下app.registry的初始化過程,從上面的額代碼來看:

    app.registry=storage.NewRegistry(app.Context, app.driver, options...)

直接看storage.NewRegistry:

    NewRegistry(ctx context.Context, driver storagedriver.StorageDriver, options ...RegistryOption) (distribution.Namespace, error) {
        // create global statter
        statter := &blobStatter{
            driver: driver,
        }

        bs := &blobStore{
            driver:  driver,
            statter: statter,
        }

        registry := &registry{
            blobStore: bs,
            blobServer: &blobServer{
                driver:  driver,
                statter: statter,
                pathFn:  bs.path,
            },
            statter:                statter,
            resumableDigestEnabled: true,
        }

        for _, option := range options {
            if err := option(registry); err != nil {
                return nil, err
            }
        }

        return registry, nil
    }

這個函數非常簡單明瞭, 基本上沒有啥函數調用,僅僅只是結構體賦值, 這裏賦值所有對象前面都初始化過了。 這個函數的內容非常重要,是後期request處理的時候找到存儲的最終的結構之一。

app.RegisterHealthChecks()

沒做分析

configureReporting(app)

該函數的內容非常簡單,但卻非常重要且關鍵。 主要就是定義了handler = app, 然後將handler返回,從而完成了從app到handler的轉換。 這裏就不再貼出它的代碼了。

alive(“/”, handler)

這個函數定義了一個”/”爲請求URL路徑的一個http request的handler函數,其實是registry的探活接口,非常簡單。

health.Handler(handler)

沒做分析

panicHandler(handler)

該函數定義了再http請求處理過程中出錯的時候會拋出panic,從而重定向到這裏。
該函數裏面回調recover()從而不會panic。


至此APP, handler構建完成,包括其對應的各種request請求的handler,後端存儲初始化都已完成。
接下來:

        server := &http.Server{
            Handler: handler,
        }

        return &Registry{
            app:    app,
            config: config,
            server: server,
        }, nil

這幾行代碼定義了http server以及Registry對象,所有的初始化工作已全部做完,舉例服務的運行僅僅剩下http server的ListenAndServe了。

registry.ListenAndServe()

這個函數的內容之前的”golang http源碼解析”分析過,就不再重複了。


至此Distribution的Start Up過程分析完成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章