前言
看 k8s 源代码有一段时间,总感觉在迷宫里乱窜,有时候觉得终于找到出口了,一下子又撞墙了,总结下来还是自己的内功不够深厚,本文是对 kube-apiserver Legacy(遗留,即将废弃)API 初始化流程(以及数据结构)的一个梳理,算是做个"路标",便于以后在"迷宫"中还能找到回来的路
kube-apiserver 主要功能是提供 api 接口给客户端访问 后端 etcd 存储,当然这中间不光是简单的 key/value 存储,为了方便扩展,kube-apiserver 设计了一套代码框架将 "资源对象" 映射到 RESTful API
本文梳理了一下 kube-apiserver 启动流程和相关数据结构
k8s 代码更新比较快,本文基于 k8s release-1.9.x 代码
数据结构
kube-apiserver 启动流程相关的(主要)数据结构
- XXXOptions 命令行参数相关的类
- XXXConfig 配置相关的类
- XXXServer kube-apiserver 服务对象
- XXXStorage 资源对象,比如 PosStorage,通过 XXXStorage 操作后端存储(etcd)
Options,Config 和 Server 对象的(大致)关系:
由 Options 对象创建(配置) Config 对象,然后由 Config 对象创建(配置) Server 对象
ServerRunOptions
ServerRunOptions 类封装了 kube-apiserver 命令行参数,这些参数按照不同的类别进行分组
// kubernetes/cmd/kube-apiserver/app/options/options.go
import (
...
genericoptions "k8s.io/apiserver/pkg/server/options"
...
)
type ServerRunOptions struct {
GenericServerRunOptions *genericoptions.ServerRunOptions
Etcd *genericoptions.EtcdOptions
SecureServing *genericoptions.SecureServingOptions
InsecureServing *kubeoptions.InsecureServingOptions
...
}
我们重点看看 EtcdOptions,etcd 存储相关的配置,它和 kube-apiserver 如何访问后端 etcd 存储密切相关
EtcdOptions
StorageConfig 定义了 etcd 详细配置,比如 etcd 版本,key 公共前缀等
// kubernetes/vendor/k8s.io/apiserver/pkg/server/options/etcd.go
type EtcdOptions struct {
StorageConfig storagebackend.Config
EncryptionProviderConfigFilepath string
...
}
// kubernetes/vender/k8s.io/apiserver/storage/storagebackend/config.o
type Config struct {
// Type defines the type of storage backend, e.g. "etcd2", "etcd3"
Type string
// Prefix is the prefix to all keys passed to storage.Interface methods.
Prefix string
// ServerList is the list of storage servers to connect with
...
}
创建
《工厂方法》NewEtcdOptions 用于创建 EtcdOptions
// kubernetes/cmd/kube-apiserver/app/options/options.go
func NewServerRunOptions() *ServerRunOptions {
s := ServerRunOptions {
...
Etcd: genericoptions.NewEtcdOptions(
storagebackend.NewDefaultConfig(kubeoptions.DefaultEtcdPathPrefix, nil))
...
}
}
ApplyWithStorageFactoryTo
EtcdOptions 有一个非常重要的方法 ApplyWithStorageFactoryTo 用于初始化 server.Config(服务配置类,见下文)的 RESTOptionsGetter 属性,该属性用于创建 etcd 后端存储(见下文)
// kubernetes/vendor/k8s.io/apiserver/pkg/server/options/etcd.go
func (s *EtcdOptions) ApplyWithStorageFactoryTo(
factory server storage.StorageFactory, c *server.Config) error {
s.RESTOptionsGetter = &storageFactoryRestOptionsFactory{
Options: *s,
StorageFactory: factory
}
}
// kubernetes/pkg/kubeapiserver/options/storage_versions.go
const (
DefaultEtcdPathPrefix = "/registry"
)
server.Config
Config is a structure used to configure a GenericAPIServer
server.Config 类用于配置和创建 GenericAPIServer(见下文)
// kubernetes/vender/k8s.io/apiserver/pkg/server/config.go
type Config struct {
...
// 这个字段很重要,标记一下
RESTOptionsGetter generic registry.RESTOptionsGetter
...
}
创建
《工厂方法》NewConfig 创建 server.Config 实例,该方法只是给 server.Config 的一些字段赋予一些默认值,《构建方法》BuildGenericConfig 对剩余的字段进行初始化
func BuildGenericConfig(s *options.ServerRunOptions, proxyTransport *http.Transport) (...) {
//《工厂方法》
genericConfig := genericapiserver.NewConfig(legacy scheme.Codecs)
...
// 将 ServerRunOptions.SecureServing "应用" 到 server.Config
if err := s.SecureServing.ApplyTo(genericConfig); err 1= nil {
...
}
}
方法体中还有很多 ApplyTo 函数调用,基本都是将 ServerRunOptions 中的字段 "应用" 到 server.Config
GenericAPIServer
GenericAPIServer contains state for a Kubernetes cluster api server
GenericAPIServer 结构体包含 server "通用" 的状态(字段),一般做为字段内嵌在具体 server(Master, APIAggregator .etc)中,可以把 GenericAPIServer 看作一个基类
type GenericAPIServer struct {
...
}
Master
Master contains state for a Kubernetes cluster master/api server
Master 结构体包含 GenericAPIServer 的引用,下面将要介绍的几个其它的结构体也都具有类似的结构
type Master struct {
GenericAPIServer *genericapiserver.GenericAPIServer
ClientCARegistrationHook ClientCARegistrationHook
}
APIAggregator
APIAggregator contains state for a Kubernetes cluster master/api server
type APIAggregator struct {
GenericAPIServer *genericapiserver.GenericAPIServer
...
}
kube-apiserver 启动流程
kube-apiserver 入口在 kubernetes/cmd/kube-apiserver/apiserver.go
PS:通过以下命令可以(单独)构建 kube-apiserver
# export GOPATH=/opt/kubernetes-src/
# cd $GOPATH/src/k8s.io/kubernetes
# make WHAT=cmd/kube-apiserver all
// kubernetes/cmd/kube-apiserver/apiserver.go
import(
...
"k8s.io.kubernetes/cmd/kube-apiserver/app"
...
)
func main() {
rand.Seed(time.Now().UTC().UnixNano())
// 解析命令行参数
s := options.NewServerRunOptions()
s.AddFlags(pflag.CommandLine)
flag.InitFlags()
// 初始化 log
logs.InitLogs()
defer logs.FlushLogs()
...
stopCh := server.SetupSignalHandler()
if err := app.Run(s, stopCh); err != nil {
fmt.FPrintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
main 函数只是个壳子,解析完命令行参数后会调用 app 包中的 Run 函数
// kubernetes/cmd/kube-apiserver/app.server.go
func Run(runOptions *options.ServerRunOptions, stopCh <-chan struct{}) error {
...
server, err := CreateServerChain(runOptions, stopCh)
if err != nil {
return err
}
return server.PrepareRun().Run(stopCh)
}
CreateServerChain 函数使用了《责任链》模式,多个 server 组成一个链条,紧密相连,给客户端提供 RESTful API 服务
上回讲到 Run 方法:
// kubernetes/cmd/kube-apiserver/app.server.go
func Run(runOptions *options.ServerRunOptions, stopCh <-chan struct{}) error {
...
server, err := CreateServerChain(runOptions, stopCh)
if err != nil {
return err
}
return server.PrepareRun().Run(stopCh)
}
目前系统中有以下 api server:
- CustomResourceDefinitions
- Master
- APIAggregator
每个 api server 都对应一个 Config(配置)
- apiextensionsapiserver.Config
- master.Config
- aggregatorapiserver.Config
CreateServerChain 的任务就是根据 ServerRunOptions 创建 XXXConfig,然后再用 XXXConfig 创建 api server,各个 api server 通过 GenericAPIServer 的 delegationTarget 字段组成《责任链》
以 Master api server 创建为例:
func CreateServerChain(
runOptions *options.ServerRunOptions, stopCh <-chan struct{})
(*genericapiserver.GenericAPIServer, error) {
...
kubeAPIServerConfig, ... := CreateKubeAPIServerConfig(...)
...
kubeAPIServer, err := CreateKubeAPIServer(kubeAPIServerConfig,
apiExtensionsServer.GenericAPIServer, sharedInformers, versionedInformers)
...
}
下面将简要介绍 Master api server 的创建过程,主要分析 kube-apiserver 是如何将 资源对象(Node,Pod,Service 等)绑定到具体的 RESTful API,使得客户端可以通过 RESTful API 操作资源对象
如果是你会怎么做?
在大概看了一些源代码之后,我不禁问自己:如果是你来设计代码架构,你会怎么做?
例如给定一个实体 Student(Java 伪代码,下同),持久化在 etcd 里
public class Student {
public int id;
public String name;
public String phone;
}
如何提供 RESTful api 接口提供对 Student 的 CRUD 操作? 设计代码框架使之适应所有的实体
api 接口示例:
PUT: /user?id=xxx&name=yyy&phone=zzz
DELETE: /user?id=xxx
POST: /user?id=xxx&name=yyy
GET: /user?id=xxx
我们分几步来考虑,首先考虑持久化,为了支持不同的持久化框架,或者即时我们就使用一种持久化框架也需要考虑框架版本匹配问题,这就需要将对持久化框架的基本操作进行抽象,抽取出接口 Backend
public interface Backend {
String get(String key);
void set(String key, String value);
}
然后我们有具体的实现类 EtcdBackend, ConsulBackend 以及 工厂类 BackendFactory
public class EtcdBackend implements Backend {
public String get(String key) { ... }
public void set(String key, String value) { ... }
}
public class ConsulBackend implements Backend {
public String get(String key) { ... }
public void set(String key, String value) { ... }
}
public class BackendFactory {
Backend get(String name) { ... }
}
Backend 搞定了,现在我们需要一个 DAO(Data access object)来访问它
public class UserDao {
private Backend backend;
// CRUD 方法
...
}
我们注意到会有很多实体,他们都需要使用 Backend 接口访问后端存储,所以可以搞个基类 AbstractDao,将 backedn 字段移到基类里头
pubic class AbstractDao {
private Backend backend;
}
public class User extends AbstractDao {
// CRUD 方法
...
}
进一步观察,其实各个 DAO 的 CRUD 方法也有很多重复的(模版)代码,比如如果我们能够封装以下变化点:
DAO 中的 CRUD 方法可以进一步抽取到 AbstractDao 中,那些实在需要子类特例化的方法,可以通过《模版方法》模式来实现
public class AbstractDao {
private Backend backend;
// CRUD 方法
...
}
public class UserDao extends AbstractDao {
// Template 方法
...
}
我们现在离最后的完工又近了一步,还剩一个问题,就是如何将 url 和 DAO 对应起来,这是一个映射问题,可以使用 map 来保持 url 对应的 DAO
map.put("/user", userDao)
以上只是一个简单的推导,k8s 的实现远比这个 demo 复杂的多,考虑到各种解耦和扩展性,下回将正式介绍 k8s 的实现