kubernetes中api-server的HTTPS服务启动流程

55 阅读5分钟

按照我目前看的k8s的v1.34版本,其中源码有上百万行,想要系统的学习源码是不太可能的事情,因此我喜欢使用每次熟悉一个功能点的方式学习源码,这样可以把其他的复杂逻辑,都先跳过去。我们知道apiserver是通过https的方式,和外部进行通信。那么apiserver中的https服务是如何启动的。本文我们就基于v1.34版本的代码看一下代码流程。

启动流程

  1. kubernetes的启动位置都在cmd目录下,所以自然apiserver的启动脚本在cmd/kube-apiserver/apiserver.go文件
  2. 由于命令行是使用的cobra库,那么我们就直接看RunE方法的逻辑,方法内大部分初始化内容我们都不关心,只看到了最后又跳入了一个Run方法
  3. Run方法内的逻辑比较清晰,就是先初始化配置、预执行、执行的流程
  4. 跳入到执行函数prepare.Run(ctx),方法内的逻辑都比较简单,就是一直跳转,最终执行到了staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go 中的如下函数内
func (s preparedGenericAPIServer) Run(stopCh <-chan struct{}) error {
    ctx := wait.ContextForChannel(stopCh)
    return s.RunWithContext(ctx)
}

RunWithContext(ctx) 方法内执行了如下代码

stoppedCh, listenerStoppedCh, err := s.NonBlockingRunWithContext(stopHTTPServerCtx, shutdownTimeout)
if err != nil {
    return err
}

NonBlockingRunWithContext() 方法内执行了如下代码

if s.SecureServingInfo != nil && s.Handler != nil {
    var err error
    stoppedCh, listenerStoppedCh, err = s.SecureServingInfo.Serve(s.Handler, shutdownTimeout, internalStopCh)
    if err != nil {
       close(internalStopCh)
       return nil, nil, err
    }
}

s.SecureServingInfo.Serve 方法内执行了如下代码

secureServer := &http.Server{
    Addr:           s.Listener.Addr().String(),
    Handler:        handler,
    MaxHeaderBytes: 1 << 20,
    TLSConfig:      tlsConfig,

    IdleTimeout:       90 * time.Second, // matches http.DefaultTransport keep-alive timeout
    ReadHeaderTimeout: 32 * time.Second, // just shy of requestTimeoutUpperBound
}

//......
// apply settings to the server
if err := http2.ConfigureServer(secureServer, http2Options); err != nil {
    return nil, nil, fmt.Errorf("error configuring http2: %v", err)
}

到这里我们可以看到http服务起来了,是使用的golang.org/x/net/http 库启动的http服务。 看到这里服务是起来了,但是我们还有两个疑问:

  1. secureServer.Addr 这个参数是从哪里传进来的?
  2. secureServer.Handler 这个http最核心的处理业务逻辑的handler到底在哪里?

apiserver的HTTPS的地址配置

在前面我们已经在staging/src/k8s.io/apiserver/pkg/server/secure_serving.goSecureServingInfo.Serve 方法中找到了最终http服务启动时使用的地址,既然我们要找哪里赋值过来的,就使用IED的跳转功能,向上一层层追溯。在staging/src/k8s.io/apiserver/pkg/server/options/serving.go 文件中,我们看到了赋值代码

func (s *SecureServingOptions) ApplyTo(config **server.SecureServingInfo) error {
    if s == nil {
       return nil
    }
    if s.BindPort <= 0 && s.Listener == nil {
       return nil
    }

    if s.Listener == nil {
       var err error
       addr := net.JoinHostPort(s.BindAddress.String(), strconv.Itoa(s.BindPort))

       c := net.ListenConfig{}

       ctls := multipleControls{}
       if s.PermitPortSharing {
          ctls = append(ctls, permitPortReuse)
       }
       if s.PermitAddressSharing {
          ctls = append(ctls, permitAddressReuse)
       }
       if len(ctls) > 0 {
          c.Control = ctls.Control
       }

       s.Listener, s.BindPort, err = CreateListener(s.BindNetwork, addr, c)
       if err != nil {
          return fmt.Errorf("failed to create listener: %v", err)
       }
    } else {
       if _, ok := s.Listener.Addr().(*net.TCPAddr); !ok {
          return fmt.Errorf("failed to parse ip and port from listener")
       }
       s.BindPort = s.Listener.Addr().(*net.TCPAddr).Port
       s.BindAddress = s.Listener.Addr().(*net.TCPAddr).IP
    }

    *config = &server.SecureServingInfo{
       Listener:                     s.Listener,
       HTTP2MaxStreamsPerConnection: s.HTTP2MaxStreamsPerConnection,
       DisableHTTP2:                 s.DisableHTTP2Serving,
    }
    // ...
}

这里有两个逻辑,如果s.Lisener == nil 不存在,就使用s.BindAddresss.BindPort两个字段来拼接地址, 那我们就继续追溯,看下这两个字段的值,哪里来的。在staging/src/k8s.io/apiserver/pkg/server/serving.go 文件中,看到了如下赋值代码

func (s *SecureServingOptions) AddFlags(fs *pflag.FlagSet) {
    if s == nil {
       return
    }

    fs.IPVar(&s.BindAddress, "bind-address", s.BindAddress, ""+
       "The IP address on which to listen for the --secure-port port. The "+
       "associated interface(s) must be reachable by the rest of the cluster, and by CLI/web "+
       "clients. If blank or an unspecified address (0.0.0.0 or ::), all interfaces and IP address families will be used.")

    desc := "The port on which to serve HTTPS with authentication and authorization."
    if s.Required {
       desc += " It cannot be switched off with 0."
    } else {
       desc += " If 0, don't serve HTTPS at all."
    }
    fs.IntVar(&s.BindPort, "secure-port", s.BindPort, desc)
}

通过这里可以看到,我们可以命令行启动时赋值bind-addressbing-port 字段来设置http服务的启动地址。 那如果没有外部设置值时,默认值是什么呢,继续查看设置值的代码,找到了pkg/kubeapiserver/options/serving.go 如下代码:

func NewSecureServingOptions() *genericoptions.SecureServingOptionsWithLoopback {
    o := genericoptions.SecureServingOptions{
       BindAddress: netutils.ParseIPSloppy("0.0.0.0"),
       BindPort:    6443,
       Required:    true,
       ServerCert: genericoptions.GeneratableKeyCert{
          PairName:      "apiserver",
          CertDirectory: "/var/run/kubernetes",
       },
    }
    return o.WithLoopback()
}

到这里我们就知道了,apiserver的https服务地址都是怎么来的了

Handler的具体业务逻辑在哪里

在前面追踪http服务启动流程时,看到了http服务时通过接收一个handler参数来启动,那么这个handler参数是哪里来的,我们也通过IED一步步向上跳转追踪,在staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go 文件发下如下代码

func (s preparedGenericAPIServer) NonBlockingRunWithContext(ctx context.Context, shutdownTimeout time.Duration) (<-chan struct{}, <-chan struct{}, error) {
    // ....
    if s.SecureServingInfo != nil && s.Handler != nil {
       var err error
       stoppedCh, listenerStoppedCh, err = s.SecureServingInfo.Serve(s.Handler, shutdownTimeout, internalStopCh)
       if err != nil {
          close(internalStopCh)
          return nil, nil, err
       }
    }
    // .....
}

通过这里知道是preparedGenericAPIServer.Handler赋值的,继续向上找,谁赋值给了它,在同文件,发现如下代码

func (s *GenericAPIServer) PrepareRun() preparedGenericAPIServer {
    // .....

    return preparedGenericAPIServer{s}
}

也就是说prepareGenericAPIServer.Handler = GenericAPIServer.Handler ,那继续找GenericAPIServer.Handler 是哪里赋值过来的,在staging/src/k8s.io/apiserver/pkg/server/config.go 中,有如下代码:

func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*GenericAPIServer, error) {
// ...
handlerChainBuilder := func(handler http.Handler) http.Handler {
    return c.BuildHandlerChainFunc(handler, c.Config)
}

// ...

apiServerHandler := NewAPIServerHandler(name, c.Serializer, handlerChainBuilder, delegationTarget.UnprotectedHandler())
s := &GenericAPIServer{
    Handler:                        apiServerHandler,
}
// ...

继续看NewAPIServerHandler方法的实现

func NewAPIServerHandler(name string, s runtime.NegotiatedSerializer, handlerChainBuilder HandlerChainBuilderFn, notFoundHandler http.Handler) *APIServerHandler {
    director := director{
       name:               name,
       goRestfulContainer: gorestfulContainer,
       nonGoRestfulMux:    nonGoRestfulMux,
    }

    return &APIServerHandler{
       FullHandlerChain:   handlerChainBuilder(director),
       GoRestfulContainer: gorestfulContainer,
       NonGoRestfulMux:    nonGoRestfulMux,
       Director:           director,
    }
}

我们算是找到了handler逻辑的最终位置,我们知道http服务的handler都需要实现一个ServerHTTP方法,我们看下APIServerHandler.ServerHTTP方法的实现

func (a *APIServerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    a.FullHandlerChain.ServeHTTP(w, r)
}

我们看到真正执行逻辑的是FullHandlerChain , 而FullHandlerChain 是由handlerChainBuilder(director) 方法创建的,查看代码

handlerChainBuilder := func(handler http.Handler) http.Handler {
    return c.BuildHandlerChainFunc(handler, c.Config)
}

继续查看BuildHandlerChainFunc 方法,发现是使用的staging/src/k8s.io/apisever/pkg/server/config.go 文件DefaultBuildHandlerChain 方法 到这里就清楚了,一个https请求过来后,apiserver是在什么位置处理逻辑的

结论

  1. apiserver的https服务的默认地址是0.0.0.0:6443 ,也可以通过命令行参数bind-address , bind-port 来自定义地址
  2. http的请求,最终都会落到staging/src/k8s.io/apiserver/pkg/server/config.go 文件下的DefaultBuildHandlerChain 方法内