1. 前言
像分析docker1.9的源码时一样,我在distribution源码分析系列的第一篇仍然是介绍主函数开始的启动流程,这样就能对distribution的大体框架有个初步了解。分析流程中,distribution的版本均为2.1.0。
2. 本文分析内容安排
- Cmd的初始化
- Execute函数
- handler句柄转发
3. Cmd的初始化
主函数在cmd/registry/main.go中,只一条语句,registry.Cmd.Execute()
,Cmd是在/distribution/registry/registry.go中定义的,所以也可以说后来registry的执行都是在该文件中定义的Cmd和其他的诸如Context上下文等的基础上执行的。再将具体初始化之前首先介绍一下启动registry是指定的参数yaml文件config-example.yml
,下面是一个具体例子:
version: 0.1
log:
fields:
service: registry
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
可见storage、http等都在此做了配置。
3.1 Cmd配置初始化
Cmd主要的初始化都是针对于Run函数变量进行的,下面的代码都是Run定义的函数中摘下来的
// 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)
}
这段代码首先定义上下文,然后解析运行registry输入的yaml参数中的内容为Configuration结构,Configuration的定义在distribution/configuration/configuration.go中,包含了运行registry需要的Log、Storage、Auth、HTTP、Notification、Redis、Health等所有配置。
配置工作已经做完了,下面开始正式建立registry并提供服务的代码,在此之前先介绍Registry结构,它代表了一个registry完整的实例,包括Configuration代表了配置信息、handlers.App代表了一个全局的registry application对象可以向所有的requests提供共享资源、http.Server定义了运行Http server的所有变量。
type Registry struct {
config *configuration.Configuration
app *handlers.App
server *http.Server
}
其中handlers.App需要单独列出来讲一下,其中mux.Router是路由分发的,storagedriver.StorageDriver用于指定后端存储,events包含了与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
// events contains notification related configuration.
events struct {
sink notifications.Sink
source notifications.SourceRecord
}
redis *redis.Pool
// true if this registry is configured as a pull through cache
isCache bool
}
介绍完了用到的重要结构体继续介绍registry启动流程
registry, err := NewRegistry(ctx, config)
if err != nil {
log.Fatalln(err)
}
if err = registry.ListenAndServe(); err != nil {
log.Fatalln(err)
}
3.2初始化App
NewRegistry
根据之前定义的Context和Configuration新建一个Regsitry,其中主要是通过handlers.NewApp给app赋值,该函数的实现位于/distribution/registry/handlers/app.go中,首先根据既有的Context和Configuration做最初的初始化
app := &App{
Config: configuration,
Context: ctx,
router: v2.RouterWithPrefix(configuration.HTTP.Prefix),
isCache: configuration.Proxy.RemoteURL != "",
}
之后调用App的register函数,通过route name将相应的handler注册到app中,register函数根据route name提供一个服务于request的handler并调用dispath指定的函数提供Http服务。
// 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)
接下来是注册storagedriver,涉及到内容比较多,稍后再讲。现在要说的是为registry的App配置secret、events、redis和loghook,代码如下:
app.configureSecret(configuration)
app.configureEvents(configuration) app.configureRedis(configuration)
app.configureLogHook(configuration)
这四个函数的实现依然都在distribution/registry/handlers/app.go中,其中,configureSecret当yaml文件中没有指定secret时会生成一个HTTP secret;configureEvents为接下来的action准备sink,主要的代码有:
for _, endpoint := range configuration.Notifications.Endpoints {
if endpoint.Disabled {
ctxu.GetLogger(app).Infof("endpoint %s disabled, skipping", endpoint.Name)
continue
}
ctxu.GetLogger(app).Infof("configuring endpoint %v (%v), timeout=%s, headers=%v", endpoint.Name, endpoint.URL, endpoint.Timeout, endpoint.Headers)
endpoint := notifications.NewEndpoint(endpoint.Name, endpoint.URL, notifications.EndpointConfig{
Timeout: endpoint.Timeout,
Threshold: endpoint.Threshold,
Backoff: endpoint.Backoff,
Headers: endpoint.Headers,
})
sinks = append(sinks, endpoint)
}
app.events.sink = notifications.NewBroadcaster(sinks...)
这段代码将registry的所有endpoint在sinks里面做注册,并且赋值给app。
hostname, err := os.Hostname()
if err != nil {
hostname = configuration.HTTP.Addr
} else {
// try to pick the port off the config
_, port, err := net.SplitHostPort(configuration.HTTP.Addr)
if err == nil {
hostname = net.JoinHostPort(hostname, port)
}
}
app.events.source = notifications.SourceRecord{
Addr: hostname,
InstanceID: ctxu.GetStringValue(app, "instance.id"),
}
首先获取主机名,通过os包直接读取主机名,当读取失败时采用从Configuration中存的Addr,之后为app.events的事件源设置为本机的主机名。
接下来介绍初始化App过程中与storigedriver相关的部分,首先根据给定的name和parameters新建一个StorageDriver,其中name指的是driver type,比如本地文件系统或者s3;parameters指的是yaml针对于该driver的配置信息
app.driver, err = factory.Create(configuration.Storage.Type(), configuration.Storage.Parameters())
之后将storage driver和middleware建立联系
app.driver, err = applyStorageMiddleware(app.driver, configuration.Middleware["storage"])
建立好和middleware的联系后,根据yaml配置存储缓存。条件语句判断yml文件是否配置了cache,如果是的话继续读取blobdescriptor的配置。接下来的switch语句根据blobdesciptor的值是redis还是inmemory来确定对应的操作,此处以inmemory为例来讲解,最重要的一行代码是storage.NewRegistry(app, app.driver, localOptions...)
,该函数的定义在distribution/registry/handlers/app.go中,该函数返回的是一个distribution.Namespace的结构,Namespace指定了一个repositories的集合,提供registry的access、trust、indexing等服务,每向registry上传一个不同名镜像,在repositories中都会有对应的一项,相当于元数据;上传同名对象时,由于tag不同,会在repositories下的tags目录中出现针对于不同tag的tags目录。但是,此处的storage.registry实现了接口Namespace的所有函数,真正返回的的storage.registry结构,描述了仓库与存储相关的变量。
// configure storage caches
if cc, ok := configuration.Storage["cache"]; ok {
v, ok := cc["blobdescriptor"]
if !ok {
// Backwards compatible: "layerinfo" == "blobdescriptor"
v = cc["layerinfo"]
}
switch v {
case "redis":
if app.redis == nil {
panic("redis configuration required to use for layerinfo cache")
}
cacheProvider := rediscache.NewRedisBlobDescriptorCacheProvider(app.redis)
localOptions := append(options, storage.BlobDescriptorCacheProvider(cacheProvider))
app.registry, err = storage.NewRegistry(app, app.driver, localOptions...)
if err != nil {
panic("could not create registry: " + err.Error())
}
ctxu.GetLogger(app).Infof("using redis blob descriptor cache")
case "inmemory":
cacheProvider := memorycache.NewInMemoryBlobDescriptorCacheProvider()
localOptions := append(options, storage.BlobDescriptorCacheProvider(cacheProvider))
app.registry, err = storage.NewRegistry(app, app.driver, localOptions...)
if err != nil {
panic("could not create registry: " + err.Error())
}
ctxu.GetLogger(app).Infof("using inmemory blob descriptor cache")
default:
if v != "" {
ctxu.GetLogger(app).Warnf("unknown cache type %q, caching disabled", configuration.Storage["cache"])
}
}
}
再向下就是配置app.accessController等,然后返回配置好的app
3.3 初始化http.Server
这块儿主要是配置Server中的Handler,代码如下:
handler := configureReporting(app)
handler = alive("/", handler)
handler = health.Handler(handler)
handler = panicHandler(handler)
handler = gorhandlers.CombinedLoggingHandler(os.Stdout, handler)
server := &http.Server{
Handler: handler,
}
第一行调用的是configureReporting函数,定义http.Handler类型的handler变量,并为之赋值app,因为App实现了ServeHTTP函数,所以可以指向app,并且拥有app具有的所有属性,主要是给handler加上了APIKey和LicenseKey。
之后调用alive函数,该函数原型为func alive(path string, handler http.Handler) http.Handler ,当path与r.URL.Path匹配时范围StatusOK;否则,返回handler.ServeHTTP(w,r),让该handler提供http服务,在该位置并没有提前定义path,所以可以认为是为了提供http服务才调用该函数的。
接下来调用health.Handler来进行安全性检测,如果失败则停止,如果通过则继续提供http服务。health.Handler(handler)中调用CheckStatus()函数,只有一行DefaultRegistry.CheckStatus(),返回默认registry的检查结果,默认registry即在用的仓库,检查仓库是否可以安全地提供服务。
之后继续完成完成后面的panicHandler唤醒延迟服务并按照yaml中配置的log选项显示log信息,CombinedLoggingHandler函数将logs以设定的Format显示。之后将根据配置好的handler给Server赋值。
至此,Registry结构中的所有属性都已配置好,可以返回了,如下所示:
return &Registry{
app: app,
config: config,
server: server,
}, nil
3.4 ListenAndServe提供服务
通过上面的代码已经完全配置好了registry,接下来的代码是registry.ListenAndServe(),这个函数运行registry的Http server,具体提供服务,流程如下:
ln, err := listener.NewListener(config.HTTP.Net, config.HTTP.Addr)
NewListener的实现位于distribution/registry/listener/listener.go中,参数是string类型的net和laddr,net指明是”unix”还是“tcp”,laddr指明提供服务的地址。根据类型分别选择提供tcp或者unix的Listen服务,比如net.Listen(“tcp”,laddr)。
之后就是配置tls,包括Key、Certificate、CA等,配置完后调用的函数是ln = tls.NewListener(ln, tlsConf)
,这个语句将原本的listener加上tls。最后返回的语句为registry.server.Serve(ln)
,之前已经为server注册了handler, 此处调用该Serve函数,以Listener为参数,就能在Listener中记录的特定地址接受连接,为每个连接新建一个goroutine。并用该协程读取request,调用相应的handler进行处理。
4. Execute函数
Execute函数的实现位于Goeps的spf13/cobra/command.go中,是Command结构的一个成员函数,首先对Command结构作介绍,它代表了程序执行的一个命令,比如go run
中的run
便是一个Command,在该结构中,Run函数是真正执行工作的:
type Command struct {
// Name is the command name, usually the executable's name.
name string
// The one-line usage message.
Use string
// An array of aliases that can be used instead of the first word in Use.
Aliases []string
// The short description shown in the 'help' output.
Short string
// The long message shown in the 'help <this-command>' output.
Long string
// Examples of how to use the command
Example string
// List of all valid non-flag arguments, used for bash completions *TODO* actually validate these
ValidArgs []string
// Custom functions used by the bash autocompletion generator
BashCompletionFunction string
// Is this command deprecated and should print this string when used?
Deprecated string
// Full set of flags
flags *flag.FlagSet
// Set of flags childrens of this command will inherit
pflags *flag.FlagSet
// Flags that are declared specifically by this command (not inherited).
lflags *flag.FlagSet
// The *Run functions are executed in the following order:
// * PersistentPreRun()
// * PreRun()
// * Run()
// * PostRun()
// * PersistentPostRun()
// All functions get the same args, the arguments after the command name
// PersistentPreRun: children of this command will inherit and execute
PersistentPreRun func(cmd *Command, args []string)
// PreRun: children of this command will not inherit.
PreRun func(cmd *Command, args []string)
// Run: Typically the actual work function. Most commands will only implement this
Run func(cmd *Command, args []string)
// PostRun: run after the Run command.
PostRun func(cmd *Command, args []string)
// PersistentPostRun: children of this command will inherit and execute after PostRun
PersistentPostRun func(cmd *Command, args []string)
// Commands is the list of commands supported by this program.
commands []*Command
// Parent Command for this command
parent *Command
// max lengths of commands' string lengths for use in padding
commandsMaxUseLen int
commandsMaxCommandPathLen int
commandsMaxNameLen int
flagErrorBuf *bytes.Buffer
cmdErrorBuf *bytes.Buffer
args []string // actual args parsed from flags
output *io.Writer // nil means stderr; use Out() method instead
usageFunc func(*Command) error // Usage can be defined by application
usageTemplate string // Can be defined by Application
helpTemplate string // Can be defined by Application
helpFunc func(*Command, []string) // Help can be defined by application
helpCommand *Command // The help command
helpFlagVal bool
// The global normalization function that we can use on every pFlag set and children commands
globNormFunc func(f *flag.FlagSet, name string) flag.NormalizedName
}
registry在启动之前做的都是一些解析参数的工作,真正的启动是从/_workspace/src/github.com/spf13/cobra/command.go中的execute里进行的,在该函数中按照PersistentPreRun(),PreRun(),Run(),PostRun(),PersistentPostRun()的顺序将五个函数执行了一遍。distribution下的configuration中保存的是配置参数,是在运行时指定的yaml文件中的。运行Cmd时,会先执行Cmd的init函数显示版本号等信息,然后执行Execute,执行到Run函数时就是第三节讲的那部分内容了。
5. handler句柄转发
与句柄转发相关的路由在handler.App结构中,所以句柄的注册是在distribution/registry/registry.go中NewRegistry函数中通过app := handlers.NewApp(ctx, config)
实现的,在这行代码之后虽然接连出现handler变量,但只是做辅助性的判断并在此基础上生成http.Server用的,并没有提供注册handler具体函数的功能。
下面就进入NewApp函数内部查看具体的注册流程,用到的函数为registry,函数的原型如下:
func (app *App) register(routeName string, dispatch dispatchFunc) {
app.router.GetRoute(routeName).Handler(app.dispatcher(dispatch))
}
有两个参数,第一个routeName指定route名,比如manifest、blob、tags等,指的是提供哪种服务,服务所有类型在distribution/registry/api/v2/routes.go中定义的,如下所示:
const (
RouteNameBase = "base"
RouteNameManifest = "manifest"
RouteNameTags = "tags"
RouteNameBlob = "blob"
RouteNameBlobUpload = "blob-upload"
RouteNameBlobUploadChunk = "blob-upload-chunk"
RouteNameCatalog = "catalog"
)
而第二个参数指定的是提供该服务的具体句柄函数。函数体中只有一条语句,GetRoute函数,根据函数提供的routeName返回具体的已经注册的mux.Route,下面的代码中注册了七个Route,正是对应于以上const中定义的七个routeName。之后的Handler函数新建一个监听Bugsnag panics的http Handler。Handler函数的参数也是http.Handler,dispatchFunc读取context和request并为route返回一个handler。dispathcer为每个endpoint建立特定于请求的handlers而不是为每个request都新建一个router。
// 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)
6. 总结
本文从源码的角度分析了从registry可执行文件开始,到读取yaml配置文件,到根据配置创建handler.App和Server,最后由Configuration、App、Server组成registry结构并调用ListenAndServe函数提供http服务,接收连接并将request转到对应的handler处理并返回结果提供服务,最后又介绍了Serve服务具体是怎样转发调度handler的。
笔者认为,学习与理解registry启动并提供服务过程的源码,不仅让用户对registry的整体架构和整体流程有了了解,同时也是用户在自身的需要下针对特定模块进行深入研究和修改代码的基础。