k8s概念入门之apiserver-针对1.1.版本阅读
apiserver
k8s中最重要的一个通信节点就是apiserver,是一个中心节点连接着每一环,是kubelet,kube-proxy和control-manager的交互的中心点,提供基于API服务来管理每一步的流程,后端采用高可用的etcd等组件作为数据库来提供数据的高可用。从介绍来看,apiserver的整个架构也基于上基于传统的http服务端来实现,这对外可提供友好的接口进行二次开发。
apiserver流程
func main() {runtime.GOMAXPROCS(runtime.NumCPU())rand.Seed(time.Now().UTC().UnixNano())s := app.NewAPIServer() // 生成服务实例s.AddFlags(pflag.CommandLine)util.InitFlags()util.InitLogs()defer util.FlushLogs()verflag.PrintAndExitIfRequested()if err := s.Run(pflag.CommandLine.Args()); err != nil { // 服务端实例运行fmt.Fprintf(os.Stderr, "%v\n", err)os.Exit(1)}
}
一切都从Run函数开始运行。
// Run runs the specified APIServer. This should never exit.
func (s *APIServer) Run(_ []string) error {s.verifyClusterIPFlags()// If advertise-address is not specified, use bind-address. If bind-address// is not usable (unset, 0.0.0.0, or loopback), setDefaults() in// pkg/master/master.go will do the right thing and use the host's default// interface.if s.AdvertiseAddress == nil || s.AdvertiseAddress.IsUnspecified() {s.AdvertiseAddress = s.BindAddress}if (s.EtcdConfigFile != "" && len(s.EtcdServerList) != 0) || (s.EtcdConfigFile == "" && len(s.EtcdServerList) == 0) {glog.Fatalf("specify either --etcd-servers or --etcd-config")} // 检查etcd配置capabilities.Initialize(capabilities.Capabilities{AllowPrivileged: s.AllowPrivileged,// TODO(vmarmol): Implement support for HostNetworkSources.PrivilegedSources: capabilities.PrivilegedSources{HostNetworkSources: []string{},HostPIDSources: []string{},HostIPCSources: []string{},},PerConnectionBandwidthLimitBytesPerSec: s.MaxConnectionBytesPerSec,})cloud, err := cloudprovider.InitCloudProvider(s.CloudProvider, s.CloudConfigFile) // 初始化云服务if err != nil {glog.Fatalf("Cloud provider could not be initialized: %v", err)}// Setup tunneler if neededvar tunneler master.Tunnelervar proxyDialerFn apiserver.ProxyDialerFuncif len(s.SSHUser) > 0 { // 判断是否是SSH方式// Get ssh key distribution func, if supportedvar installSSH master.InstallSSHKeyif cloud != nil {if instances, supported := cloud.Instances(); supported {installSSH = instances.AddSSHKeyToAllInstances}}// Set up the tunnelertunneler = master.NewSSHTunneler(s.SSHUser, s.SSHKeyfile, installSSH)// Use the tunneler's dialer to connect to the kubelets.KubeletConfig.Dial = tunneler.Dial// Use the tunneler's dialer when proxying to pods, services, and nodesproxyDialerFn = tunneler.Dial}// Proxying to pods and services is IP-based... don't expect to be able to verify the hostnameproxyTLSClientConfig := &tls.Config{InsecureSkipVerify: true}kubeletClient, err := client.NewKubeletClient(&s.KubeletConfig) // 生成kubelet连接客户端if err != nil {glog.Fatalf("Failure to start kubelet client: %v", err)}apiGroupVersionOverrides, err := s.parseRuntimeConfig() // 生成api组信息if err != nil {glog.Fatalf("error in parsing runtime-config: %s", err)}clientConfig := &client.Config{Host: net.JoinHostPort(s.InsecureBindAddress.String(), strconv.Itoa(s.InsecurePort)),Version: s.DeprecatedStorageVersion,}client, err := client.New(clientConfig) if err != nil {glog.Fatalf("Invalid server address: %v", err)}legacyV1Group, err := latest.Group("") if err != nil {return err}storageDestinations := master.NewStorageDestinations() // 生成保存的实例storageVersions := generateStorageVersionMap(s.DeprecatedStorageVersion, s.StorageVersions)if _, found := storageVersions[legacyV1Group.Group]; !found {glog.Fatalf("Couldn't find the storage version for group: %q in storageVersions: %v", legacyV1Group.Group, storageVersions)} // 保存的规则etcdStorage, err := newEtcd(s.EtcdConfigFile, s.EtcdServerList, legacyV1Group.InterfacesFor, storageVersions[legacyV1Group.Group], s.EtcdPathPrefix) // 获取基于etcd的保存方式if err != nil {glog.Fatalf("Invalid storage version or misconfigured etcd: %v", err)}storageDestinations.AddAPIGroup("", etcdStorage) // 将当前组添加到通过etcd存储if !apiGroupVersionOverrides["extensions/v1beta1"].Disable {expGroup, err := latest.Group("extensions")if err != nil {glog.Fatalf("Extensions API is enabled in runtime config, but not enabled in the environment variable KUBE_API_VERSIONS. Error: %v", err)}if _, found := storageVersions[expGroup.Group]; !found {glog.Fatalf("Couldn't find the storage version for group: %q in storageVersions: %v", expGroup.Group, storageVersions)}expEtcdStorage, err := newEtcd(s.EtcdConfigFile, s.EtcdServerList, expGroup.InterfacesFor, storageVersions[expGroup.Group], s.EtcdPathPrefix)if err != nil {glog.Fatalf("Invalid extensions storage version or misconfigured etcd: %v", err)}storageDestinations.AddAPIGroup("extensions", expEtcdStorage)}updateEtcdOverrides(s.EtcdServersOverrides, storageVersions, s.EtcdPathPrefix, &storageDestinations, newEtcd) // 查看是否有更新的配置信息n := s.ServiceClusterIPRange// Default to the private server key for service account token signingif s.ServiceAccountKeyFile == "" && s.TLSPrivateKeyFile != "" {if apiserver.IsValidServiceAccountKeyFile(s.TLSPrivateKeyFile) {s.ServiceAccountKeyFile = s.TLSPrivateKeyFile} else {glog.Warning("no RSA key provided, service account token authentication disabled")}}authenticator, err := apiserver.NewAuthenticator(apiserver.AuthenticatorConfig{BasicAuthFile: s.BasicAuthFile,ClientCAFile: s.ClientCAFile,TokenAuthFile: s.TokenAuthFile,OIDCIssuerURL: s.OIDCIssuerURL,OIDCClientID: s.OIDCClientID,OIDCCAFile: s.OIDCCAFile,OIDCUsernameClaim: s.OIDCUsernameClaim,ServiceAccountKeyFile: s.ServiceAccountKeyFile,ServiceAccountLookup: s.ServiceAccountLookup,Storage: etcdStorage,KeystoneURL: s.KeystoneURL,}) // 认证方式if err != nil {glog.Fatalf("Invalid Authentication Config: %v", err)}authorizationModeNames := strings.Split(s.AuthorizationMode, ",")authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(authorizationModeNames, s.AuthorizationPolicyFile)if err != nil {glog.Fatalf("Invalid Authorization Config: %v", err)}admissionControlPluginNames := strings.Split(s.AdmissionControl, ",")admissionController := admission.NewFromPlugins(client, admissionControlPluginNames, s.AdmissionControlConfigFile)if len(s.ExternalHost) == 0 {// TODO: extend for other providersif s.CloudProvider == "gce" { // 判断是否是gce平台的cloudinstances, supported := cloud.Instances()if !supported {glog.Fatalf("gce cloud provider has no instances. this shouldn't happen. exiting.")}name, err := os.Hostname()if err != nil {glog.Fatalf("failed to get hostname: %v", err)}addrs, err := instances.NodeAddresses(name)if err != nil {glog.Warningf("unable to obtain external host address from cloud provider: %v", err)} else {for _, addr := range addrs {if addr.Type == api.NodeExternalIP {s.ExternalHost = addr.Address}}}}}config := &master.Config{StorageDestinations: storageDestinations,StorageVersions: storageVersions,EventTTL: s.EventTTL,KubeletClient: kubeletClient,ServiceClusterIPRange: &n,EnableCoreControllers: true,EnableLogsSupport: s.EnableLogsSupport,EnableUISupport: true,EnableSwaggerSupport: true,EnableProfiling: s.EnableProfiling,EnableWatchCache: s.EnableWatchCache,EnableIndex: true,APIPrefix: s.APIPrefix,APIGroupPrefix: s.APIGroupPrefix,CorsAllowedOriginList: s.CorsAllowedOriginList,ReadWritePort: s.SecurePort,PublicAddress: s.AdvertiseAddress,Authenticator: authenticator,SupportsBasicAuth: len(s.BasicAuthFile) > 0,Authorizer: authorizer,AdmissionControl: admissionController,APIGroupVersionOverrides: apiGroupVersionOverrides,MasterServiceNamespace: s.MasterServiceNamespace,ClusterName: s.ClusterName,ExternalHost: s.ExternalHost,MinRequestTimeout: s.MinRequestTimeout,ProxyDialer: proxyDialerFn,ProxyTLSClientConfig: proxyTLSClientConfig,Tunneler: tunneler,ServiceNodePortRange: s.ServiceNodePortRange,} // 生成master配置m := master.New(config) // 生成一个Master实例// We serve on 2 ports. See docs/accessing_the_api.mdsecureLocation := ""if s.SecurePort != 0 {secureLocation = net.JoinHostPort(s.BindAddress.String(), strconv.Itoa(s.SecurePort))}insecureLocation := net.JoinHostPort(s.InsecureBindAddress.String(), strconv.Itoa(s.InsecurePort))// See the flag commentary to understand our assumptions when opening the read-only and read-write ports.var sem chan boolif s.MaxRequestsInFlight > 0 {sem = make(chan bool, s.MaxRequestsInFlight)}longRunningRE := regexp.MustCompile(s.LongRunningRequestRE)longRunningTimeout := func(req *http.Request) (<-chan time.Time, string) {// TODO unify this with apiserver.MaxInFlightLimitif longRunningRE.MatchString(req.URL.Path) || req.URL.Query().Get("watch") == "true" {return nil, ""}return time.After(time.Minute), ""}if secureLocation != "" {handler := apiserver.TimeoutHandler(m.Handler, longRunningTimeout) // 设置超时handler安全的监听流程secureServer := &http.Server{Addr: secureLocation,Handler: apiserver.MaxInFlightLimit(sem, longRunningRE, apiserver.RecoverPanics(handler)),MaxHeaderBytes: 1 << 20,TLSConfig: &tls.Config{// Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability)MinVersion: tls.VersionTLS10,},}if len(s.ClientCAFile) > 0 {clientCAs, err := util.CertPoolFromFile(s.ClientCAFile)if err != nil {glog.Fatalf("unable to load client CA file: %v", err)}// Populate PeerCertificates in requests, but don't reject connections without certificates// This allows certificates to be validated by authenticators, while still allowing other auth typessecureServer.TLSConfig.ClientAuth = tls.RequestClientCert// Specify allowed CAs for client certificatessecureServer.TLSConfig.ClientCAs = clientCAs}glog.Infof("Serving securely on %s", secureLocation)if s.TLSCertFile == "" && s.TLSPrivateKeyFile == "" {s.TLSCertFile = path.Join(s.CertDirectory, "apiserver.crt")s.TLSPrivateKeyFile = path.Join(s.CertDirectory, "apiserver.key")// TODO (cjcullen): Is PublicAddress the right address to sign a cert with?alternateIPs := []net.IP{config.ServiceReadWriteIP}alternateDNS := []string{"kubernetes.default.svc", "kubernetes.default", "kubernetes"}// It would be nice to set a fqdn subject alt name, but only the kubelets know, the apiserver is clueless// alternateDNS = append(alternateDNS, "kubernetes.default.svc.CLUSTER.DNS.NAME")if err := util.GenerateSelfSignedCert(config.PublicAddress.String(), s.TLSCertFile, s.TLSPrivateKeyFile, alternateIPs, alternateDNS); err != nil {glog.Errorf("Unable to generate self signed cert: %v", err)} else {glog.Infof("Using self-signed cert (%s, %s)", s.TLSCertFile, s.TLSPrivateKeyFile)}}go func() {defer util.HandleCrash()for {// err == systemd.SdNotifyNoSocket when not running on a systemd systemif err := systemd.SdNotify("READY=1\n"); err != nil && err != systemd.SdNotifyNoSocket {glog.Errorf("Unable to send systemd daemon successful start message: %v\n", err)}if err := secureServer.ListenAndServeTLS(s.TLSCertFile, s.TLSPrivateKeyFile); err != nil {glog.Errorf("Unable to listen for secure (%v); will try again.", err)}time.Sleep(15 * time.Second)}}()}handler := apiserver.TimeoutHandler(m.InsecureHandler, longRunningTimeout) // 添加handlerhttp := &http.Server{Addr: insecureLocation,Handler: apiserver.RecoverPanics(handler),MaxHeaderBytes: 1 << 20,}if secureLocation == "" {// err == systemd.SdNotifyNoSocket when not running on a systemd systemif err := systemd.SdNotify("READY=1\n"); err != nil && err != systemd.SdNotifyNoSocket {glog.Errorf("Unable to send systemd daemon successful start message: %v\n", err)}}glog.Infof("Serving insecurely on %s", insecureLocation)glog.Fatal(http.ListenAndServe()) // 运行serverreturn nil
}
最主要的逻辑流程就是通过输入的参数来初始化(例如监听端口,有关服务生成的端口范围等等),生成对应服务的API接口信息,并初始化对应API接口访问数据后端的存储实例(通过etcd来存储),一切就绪后开始启动服务,有关API接口的详细信息都在master.New的函数中去实现的。
func New(c *Config) *Master {setDefaults(c)if c.KubeletClient == nil {glog.Fatalf("master.New() called with config.KubeletClient == nil")}m := &Master{serviceClusterIPRange: c.ServiceClusterIPRange, // 服务的IP范围serviceNodePortRange: c.ServiceNodePortRange, //Node的Port的范围rootWebService: new(restful.WebService),enableCoreControllers: c.EnableCoreControllers, enableLogsSupport: c.EnableLogsSupport,enableUISupport: c.EnableUISupport,enableSwaggerSupport: c.EnableSwaggerSupport,enableProfiling: c.EnableProfiling,enableWatchCache: c.EnableWatchCache,apiPrefix: c.APIPrefix, // API前缀apiGroupPrefix: c.APIGroupPrefix, // 分组前缀corsAllowedOriginList: c.CorsAllowedOriginList,authenticator: c.Authenticator,authorizer: c.Authorizer,admissionControl: c.AdmissionControl,apiGroupVersionOverrides: c.APIGroupVersionOverrides,requestContextMapper: c.RequestContextMapper,cacheTimeout: c.CacheTimeout, // 缓存超时时间minRequestTimeout: time.Duration(c.MinRequestTimeout) * time.Second,masterCount: c.MasterCount,externalHost: c.ExternalHost,clusterIP: c.PublicAddress,publicReadWritePort: c.ReadWritePort,serviceReadWriteIP: c.ServiceReadWriteIP,// TODO: serviceReadWritePort should be passed in as an argument, it may not always be 443serviceReadWritePort: 443,tunneler: c.Tunneler,}var handlerContainer *restful.Containerif c.RestfulContainer != nil {m.mux = c.RestfulContainer.ServeMuxhandlerContainer = c.RestfulContainer} else {mux := http.NewServeMux()m.mux = muxhandlerContainer = NewHandlerContainer(mux)}m.handlerContainer = handlerContainer// Use CurlyRouter to be able to use regular expressions in paths. Regular expressions are required in paths for example for proxy (where the path is proxy/{kind}/{name}/{*})m.handlerContainer.Router(restful.CurlyRouter{}) // 生成服务的实现信息 后续只需要添加到该container种m.muxHelper = &apiserver.MuxHelper{Mux: m.mux, RegisteredPaths: []string{}}m.init(c) // 初始化所有的API信息并设置存储信息return m
}...// init initializes master.
func (m *Master) init(c *Config) {if c.ProxyDialer != nil || c.ProxyTLSClientConfig != nil {m.proxyTransport = util.SetTransportDefaults(&http.Transport{Dial: c.ProxyDialer,TLSClientConfig: c.ProxyTLSClientConfig,})}healthzChecks := []healthz.HealthzChecker{} // 健康检查列表dbClient := func(resource string) storage.Interface { return c.StorageDestinations.get("", resource) }podStorage := podetcd.NewStorage(dbClient("pods"), c.EnableWatchCache, c.KubeletClient, m.proxyTransport) // 生成pods的etcd保存者podTemplateStorage := podtemplateetcd.NewREST(dbClient("podTemplates")) // 生成模板的保存者eventStorage := eventetcd.NewREST(dbClient("events"), uint64(c.EventTTL.Seconds())) // 生成事件的保存者limitRangeStorage := limitrangeetcd.NewREST(dbClient("limitRanges"))resourceQuotaStorage, resourceQuotaStatusStorage := resourcequotaetcd.NewREST(dbClient("resourceQuotas"))secretStorage := secretetcd.NewREST(dbClient("secrets"))serviceAccountStorage := serviceaccountetcd.NewREST(dbClient("serviceAccounts")) // 生成serviceAccounts的保存者persistentVolumeStorage, persistentVolumeStatusStorage := pvetcd.NewREST(dbClient("persistentVolumes")) // 生成一致容器卷的保存者persistentVolumeClaimStorage, persistentVolumeClaimStatusStorage := pvcetcd.NewREST(dbClient("persistentVolumeClaims"))namespaceStorage, namespaceStatusStorage, namespaceFinalizeStorage := namespaceetcd.NewREST(dbClient("namespaces")) //生成命名空间的保存者m.namespaceRegistry = namespace.NewRegistry(namespaceStorage)endpointsStorage := endpointsetcd.NewREST(dbClient("endpoints"), c.EnableWatchCache)m.endpointRegistry = endpoint.NewRegistry(endpointsStorage) // 生成endpoint的保存者nodeStorage, nodeStatusStorage := nodeetcd.NewREST(dbClient("nodes"), c.EnableWatchCache, c.KubeletClient, m.proxyTransport)m.nodeRegistry = node.NewRegistry(nodeStorage) // 生成node的保存者serviceStorage := serviceetcd.NewREST(dbClient("services"))m.serviceRegistry = service.NewRegistry(serviceStorage) // 生成services的保存者var serviceClusterIPRegistry service.RangeRegistryserviceClusterIPAllocator := ipallocator.NewAllocatorCIDRRange(m.serviceClusterIPRange, func(max int, rangeSpec string) allocator.Interface {mem := allocator.NewAllocationMap(max, rangeSpec)etcd := etcdallocator.NewEtcd(mem, "/ranges/serviceips", "serviceipallocation", dbClient("services"))serviceClusterIPRegistry = etcdreturn etcd})m.serviceClusterIPAllocator = serviceClusterIPRegistryvar serviceNodePortRegistry service.RangeRegistryserviceNodePortAllocator := portallocator.NewPortAllocatorCustom(m.serviceNodePortRange, func(max int, rangeSpec string) allocator.Interface {mem := allocator.NewAllocationMap(max, rangeSpec)etcd := etcdallocator.NewEtcd(mem, "/ranges/servicenodeports", "servicenodeportallocation", dbClient("services"))serviceNodePortRegistry = etcdreturn etcd})m.serviceNodePortAllocator = serviceNodePortRegistrycontrollerStorage, controllerStatusStorage := controlleretcd.NewREST(dbClient("replicationControllers")) // 生成replicationControllers的保存者// TODO: Factor out the core API registrationm.storage = map[string]rest.Storage{"pods": podStorage.Pod,"pods/attach": podStorage.Attach,"pods/status": podStorage.Status,"pods/log": podStorage.Log,"pods/exec": podStorage.Exec,"pods/portforward": podStorage.PortForward,"pods/proxy": podStorage.Proxy,"pods/binding": podStorage.Binding,"bindings": podStorage.Binding,"podTemplates": podTemplateStorage,"replicationControllers": controllerStorage,"replicationControllers/status": controllerStatusStorage,"services": service.NewStorage(m.serviceRegistry, m.endpointRegistry, serviceClusterIPAllocator, serviceNodePortAllocator, m.proxyTransport),"endpoints": endpointsStorage,"nodes": nodeStorage,"nodes/status": nodeStatusStorage,"events": eventStorage,"limitRanges": limitRangeStorage,"resourceQuotas": resourceQuotaStorage,"resourceQuotas/status": resourceQuotaStatusStorage,"namespaces": namespaceStorage,"namespaces/status": namespaceStatusStorage,"namespaces/finalize": namespaceFinalizeStorage,"secrets": secretStorage,"serviceAccounts": serviceAccountStorage,"persistentVolumes": persistentVolumeStorage,"persistentVolumes/status": persistentVolumeStatusStorage,"persistentVolumeClaims": persistentVolumeClaimStorage,"persistentVolumeClaims/status": persistentVolumeClaimStatusStorage,"componentStatuses": componentstatus.NewStorage(func() map[string]apiserver.Server { return m.getServersToValidate(c) }),} // 路由匹配数组if m.tunneler != nil {m.tunneler.Run(m.getNodeAddresses)healthzChecks = append(healthzChecks, healthz.NamedCheck("SSH Tunnel Check", m.IsTunnelSyncHealthy))prometheus.NewGaugeFunc(prometheus.GaugeOpts{Name: "apiserver_proxy_tunnel_sync_latency_secs",Help: "The time since the last successful synchronization of the SSH tunnels for proxy requests.",}, func() float64 { return float64(m.tunneler.SecondsSinceSync()) })}apiVersions := []string{} // 获取API版本信息// Install v1 unless disabled.if !m.apiGroupVersionOverrides["api/v1"].Disable {if err := m.api_v1().InstallREST(m.handlerContainer); err != nil {glog.Fatalf("Unable to setup API v1: %v", err)}apiVersions = append(apiVersions, "v1")}apiserver.InstallSupport(m.muxHelper, m.rootWebService, c.EnableProfiling, healthzChecks...)apiserver.AddApiWebService(m.handlerContainer, c.APIPrefix, apiVersions) // 注册处理信息defaultVersion := m.defaultAPIGroupVersion()requestInfoResolver := &apiserver.APIRequestInfoResolver{APIPrefixes: sets.NewString(strings.TrimPrefix(defaultVersion.Root, "/")), RestMapper: defaultVersion.Mapper}apiserver.InstallServiceErrorHandler(m.handlerContainer, requestInfoResolver, apiVersions)// allGroups records all supported groups at /apisallGroups := []unversioned.APIGroup{} // 生成所有的apis的接口信息// Install extensions unless disabled.if !m.apiGroupVersionOverrides["extensions/v1beta1"].Disable {m.thirdPartyStorage = c.StorageDestinations.APIGroups["extensions"].Defaultm.thirdPartyResources = map[string]*thirdpartyresourcedataetcd.REST{}expVersion := m.experimental(c)if err := expVersion.InstallREST(m.handlerContainer); err != nil {glog.Fatalf("Unable to setup experimental api: %v", err)}g, err := latest.Group("extensions")if err != nil {glog.Fatalf("Unable to setup experimental api: %v", err)}expAPIVersions := []unversioned.GroupVersion{{GroupVersion: expVersion.Version,Version: apiutil.GetVersion(expVersion.Version),},}storageVersion, found := c.StorageVersions[g.Group]if !found {glog.Fatalf("Couldn't find storage version of group %v", g.Group)}group := unversioned.APIGroup{Name: g.Group,Versions: expAPIVersions,PreferredVersion: unversioned.GroupVersion{GroupVersion: storageVersion, Version: apiutil.GetVersion(storageVersion)},}apiserver.AddGroupWebService(m.handlerContainer, c.APIGroupPrefix+"/"+latest.GroupOrDie("extensions").Group, group)allGroups = append(allGroups, group)expRequestInfoResolver := &apiserver.APIRequestInfoResolver{APIPrefixes: sets.NewString(strings.TrimPrefix(expVersion.Root, "/")), RestMapper: expVersion.Mapper}apiserver.InstallServiceErrorHandler(m.handlerContainer, expRequestInfoResolver, []string{expVersion.Version})}// This should be done after all groups are registered// TODO: replace the hardcoded "apis".apiserver.AddApisWebService(m.handlerContainer, "/apis", allGroups) // 注册到框架中处理// Register root handler.// We do not register this using restful Webservice since we do not want to surface this in api docs.// Allow master to be embedded in contexts which already have something registered at the rootif c.EnableIndex {m.mux.HandleFunc("/", apiserver.IndexHandler(m.handlerContainer, m.muxHelper))}if c.EnableLogsSupport {apiserver.InstallLogsSupport(m.muxHelper)}if c.EnableUISupport {ui.InstallSupport(m.muxHelper, m.enableSwaggerSupport)}if c.EnableProfiling { // 是否启动调试接口m.mux.HandleFunc("/debug/pprof/", pprof.Index)m.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)m.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)}handler := http.Handler(m.mux.(*http.ServeMux)) // 生成处理hanndler// TODO: handle CORS and auth using go-restful// See github.com/emicklei/go-restful/blob/master/examples/restful-CORS-filter.go, and// github.com/emicklei/go-restful/blob/master/examples/restful-basic-authentication.goif len(c.CorsAllowedOriginList) > 0 {allowedOriginRegexps, err := util.CompileRegexps(c.CorsAllowedOriginList)if err != nil {glog.Fatalf("Invalid CORS allowed origin, --cors-allowed-origins flag was set to %v - %v", strings.Join(c.CorsAllowedOriginList, ","), err)}handler = apiserver.CORS(handler, allowedOriginRegexps, nil, nil, "true")}m.InsecureHandler = handler // 保存handlerattributeGetter := apiserver.NewRequestAttributeGetter(m.requestContextMapper, latest.GroupOrDie("").RESTMapper, "api")handler = apiserver.WithAuthorizationCheck(handler, attributeGetter, m.authorizer)// Install Authenticatorif c.Authenticator != nil {authenticatedHandler, err := handlers.NewRequestAuthenticator(m.requestContextMapper, c.Authenticator, handlers.Unauthorized(c.SupportsBasicAuth), handler)if err != nil {glog.Fatalf("Could not initialize authenticator: %v", err)}handler = authenticatedHandler}// Install root web servicesm.handlerContainer.Add(m.rootWebService) // 注册// TODO: Make this optional? Consumers of master depend on this currently.m.Handler = handlerif m.enableSwaggerSupport {m.InstallSwaggerAPI()}// After all wrapping is done, put a context filter around both handlersif handler, err := api.NewRequestContextFilter(m.requestContextMapper, m.Handler); err != nil {glog.Fatalf("Could not initialize request context filter: %v", err)} else {m.Handler = handler}if handler, err := api.NewRequestContextFilter(m.requestContextMapper, m.InsecureHandler); err != nil {glog.Fatalf("Could not initialize request context filter: %v", err)} else {m.InsecureHandler = handler}// TODO: Attempt clean shutdown?if m.enableCoreControllers {m.NewBootstrapController().Start()}
}
从master的初始化过程中可知,针对node、service、endpoint等相关的操作都通过了一个storage来进行处理保存,并且在每一个生成的storage中通过服务的get、post等路由操作,直接操作到了基于etcd存储的相关操作中。相关的操作的代码都位于pkg/registry目录下面。
API的注册流程
// InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container.
// It is expected that the provided path root prefix will serve all operations. Root MUST NOT end
// in a slash.
func (g *APIGroupVersion) InstallREST(container *restful.Container) error {installer := g.newInstaller() // 生成一个注册者ws := installer.NewWebService() // 获取serviceapiResources, registrationErrors := installer.Install(ws) // 注册路由信息// TODO: g.Version only contains "version" now, it will contain "group/version" in the near future.AddSupportedResourcesWebService(ws, g.Version, apiResources)container.Add(ws) // 添加服务return errors.NewAggregate(registrationErrors)
}
主要的路由的写入工作都是由installer的Install流程。
// Installs handlers for API resources.
func (a *APIInstaller) Install(ws *restful.WebService) (apiResources []unversioned.APIResource, errors []error) {errors = make([]error, 0)proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.group.Storage, a.group.Codec, a.group.Context, a.info}) // Register the paths in a deterministic (sorted) order to get a deterministic swagger spec.paths := make([]string, len(a.group.Storage)) // 生成对应大小的列表var i int = 0for path := range a.group.Storage {paths[i] = pathi++}sort.Strings(paths)for _, path := range paths {apiResource, err := a.registerResourceHandlers(path, a.group.Storage[path], ws, proxyHandler) // 注册路径信息if err != nil {errors = append(errors, err)}if apiResource != nil {apiResources = append(apiResources, *apiResource)}}return apiResources, errors
}...func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService, proxyHandler http.Handler) (*unversioned.APIResource, error) {...// what verbs are supported by the storage, used to know what verbs we support per pathcreater, isCreater := storage.(rest.Creater) // 转换从storage对应过来的方法namedCreater, isNamedCreater := storage.(rest.NamedCreater)lister, isLister := storage.(rest.Lister)getter, isGetter := storage.(rest.Getter)getterWithOptions, isGetterWithOptions := storage.(rest.GetterWithOptions)deleter, isDeleter := storage.(rest.Deleter)gracefulDeleter, isGracefulDeleter := storage.(rest.GracefulDeleter)updater, isUpdater := storage.(rest.Updater)patcher, isPatcher := storage.(rest.Patcher)watcher, isWatcher := storage.(rest.Watcher)_, isRedirector := storage.(rest.Redirector)connecter, isConnecter := storage.(rest.Connecter)storageMeta, isMetadata := storage.(rest.StorageMetadata)if !isMetadata {storageMeta = defaultStorageMetadata{}}.. var ctxFn ContextFunc // 生成传递的上下文信息ctxFn = func(req *restful.Request) api.Context {if context == nil {return api.NewContext()}if ctx, ok := context.Get(req.Request); ok {return ctx}return api.NewContext()}allowWatchList := isWatcher && isLister // watching on lists is allowed only for kinds that support both watch and list.scope := mapping.Scope // 获取对应的参数信息nameParam := ws.PathParameter("name", "name of the "+kind).DataType("string")pathParam := ws.PathParameter("path", "path to the resource").DataType("string")params := []*restful.Parameter{}actions := []action{}var apiResource unversioned.APIResource// Get the list of actions for the given scope.switch scope.Name() {case meta.RESTScopeNameRoot: // 获取对应的路径名称 根据传入的路径来判断不同的资源操作// Handle non-namespace scoped resources like nodes.resourcePath := resourceresourceParams := paramsitemPath := resourcePath + "/{name}"nameParams := append(params, nameParam)proxyParams := append(nameParams, pathParam)if hasSubresource {itemPath = itemPath + "/" + subresourceresourcePath = itemPathresourceParams = nameParams}apiResource.Name = pathapiResource.Namespaced = falsenamer := rootScopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath)}// Handler for standard REST verbs (GET, PUT, POST and DELETE).// Add actions at the resource path: /api/apiVersion/resourceactions = appendIf(actions, action{"LIST", resourcePath, resourceParams, namer}, isLister) // 检查是否是list操作actions = appendIf(actions, action{"POST", resourcePath, resourceParams, namer}, isCreater) // 是否是POST操作actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, resourceParams, namer}, allowWatchList)// Add actions at the item path: /api/apiVersion/resource/{name}actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, isGetter) // 是否get操作if getSubpath {actions = appendIf(actions, action{"GET", itemPath + "/{path:*}", proxyParams, namer}, isGetter)}actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, isUpdater)actions = appendIf(actions, action{"PATCH", itemPath, nameParams, namer}, isPatcher)actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, isDeleter)actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer}, isWatcher)actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath + "/{path:*}", proxyParams, namer}, isRedirector)actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath, nameParams, namer}, isRedirector)actions = appendIf(actions, action{"CONNECT", itemPath, nameParams, namer}, isConnecter)actions = appendIf(actions, action{"CONNECT", itemPath + "/{path:*}", proxyParams, namer}, isConnecter && connectSubpath)breakcase meta.RESTScopeNameNamespace:// Handler for standard REST verbs (GET, PUT, POST and DELETE).namespaceParam := ws.PathParameter(scope.ArgumentName(), scope.ParamDescription()).DataType("string")namespacedPath := scope.ParamName() + "/{" + scope.ArgumentName() + "}/" + resourcenamespaceParams := []*restful.Parameter{namespaceParam}resourcePath := namespacedPathresourceParams := namespaceParamsitemPath := namespacedPath + "/{name}"nameParams := append(namespaceParams, nameParam)proxyParams := append(nameParams, pathParam)if hasSubresource {itemPath = itemPath + "/" + subresourceresourcePath = itemPathresourceParams = nameParams}apiResource.Name = pathapiResource.Namespaced = truenamer := scopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath), false}actions = appendIf(actions, action{"LIST", resourcePath, resourceParams, namer}, isLister)actions = appendIf(actions, action{"POST", resourcePath, resourceParams, namer}, isCreater)// DEPRECATEDactions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, resourceParams, namer}, allowWatchList)actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, isGetter)if getSubpath {actions = appendIf(actions, action{"GET", itemPath + "/{path:*}", proxyParams, namer}, isGetter)}actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, isUpdater)actions = appendIf(actions, action{"PATCH", itemPath, nameParams, namer}, isPatcher)actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, isDeleter)actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer}, isWatcher)actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath + "/{path:*}", proxyParams, namer}, isRedirector)actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath, nameParams, namer}, isRedirector)actions = appendIf(actions, action{"CONNECT", itemPath, nameParams, namer}, isConnecter)actions = appendIf(actions, action{"CONNECT", itemPath + "/{path:*}", proxyParams, namer}, isConnecter && connectSubpath)// list or post across namespace.// For ex: LIST all pods in all namespaces by sending a LIST request at /api/apiVersion/pods.// TODO: more strongly type whether a resource allows these actions on "all namespaces" (bulk delete)if !hasSubresource {namer = scopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath), true}actions = appendIf(actions, action{"LIST", resource, params, namer}, isLister)actions = appendIf(actions, action{"WATCHLIST", "watch/" + resource, params, namer}, allowWatchList)}breakdefault:return nil, fmt.Errorf("unsupported restscope: %s", scope.Name())}...for _, action := range actions { // 根据判断的actions来依次执行获取结果reqScope.Namer = action.Namerm := monitorFilter(action.Verb, resource)namespaced := ""if strings.Contains(action.Path, scope.ArgumentName()) {namespaced = "Namespaced"}switch action.Verb {case "GET": // Get a resource.var handler restful.RouteFunctionif isGetterWithOptions {handler = GetResourceWithOptions(getterWithOptions, reqScope, getOptionsKind, getSubpath, getSubpathKey)} else {handler = GetResource(getter, reqScope)}doc := "read the specified " + kindif hasSubresource {doc = "read " + subresource + " of the specified " + kind}route := ws.GET(action.Path).To(handler).Filter(m).Doc(doc).Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).Operation("read"+namespaced+kind+strings.Title(subresource)).Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).Returns(http.StatusOK, "OK", versionedObject).Writes(versionedObject)if isGetterWithOptions {if err := addObjectParams(ws, route, versionedGetOptions); err != nil {return nil, err}}addParams(route, action.Params)ws.Route(route)case "LIST": // List all resources of a kind.doc := "list objects of kind " + kindif hasSubresource {doc = "list " + subresource + " of objects of kind " + kind}route := ws.GET(action.Path).To(ListResource(lister, watcher, reqScope, false, a.minRequestTimeout)).Filter(m).Doc(doc).Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).Operation("list"+namespaced+kind+strings.Title(subresource)).Produces("application/json").Returns(http.StatusOK, "OK", versionedList).Writes(versionedList)if err := addObjectParams(ws, route, versionedListOptions); err != nil {return nil, err}switch {case isLister && isWatcher:doc := "list or watch objects of kind " + kindif hasSubresource {doc = "list or watch " + subresource + " of objects of kind " + kind}route.Doc(doc)case isWatcher:doc := "watch objects of kind " + kindif hasSubresource {doc = "watch " + subresource + "of objects of kind " + kind}route.Doc(doc)}addParams(route, action.Params)ws.Route(route)case "PUT": // Update a resource.doc := "replace the specified " + kindif hasSubresource {doc = "replace " + subresource + " of the specified " + kind}route := ws.PUT(action.Path).To(UpdateResource(updater, reqScope, a.group.Typer, admit)).Filter(m).Doc(doc).Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).Operation("replace"+namespaced+kind+strings.Title(subresource)).Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).Returns(http.StatusOK, "OK", versionedObject).Reads(versionedObject).Writes(versionedObject)addParams(route, action.Params)ws.Route(route)case "PATCH": // Partially update a resourcedoc := "partially update the specified " + kindif hasSubresource {doc = "partially update " + subresource + " of the specified " + kind}route := ws.PATCH(action.Path).To(PatchResource(patcher, reqScope, a.group.Typer, admit, mapping.ObjectConvertor)).Filter(m).Doc(doc).Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).Consumes(string(api.JSONPatchType), string(api.MergePatchType), string(api.StrategicMergePatchType)).Operation("patch"+namespaced+kind+strings.Title(subresource)).Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).Returns(http.StatusOK, "OK", versionedObject).Reads(unversioned.Patch{}).Writes(versionedObject)addParams(route, action.Params)ws.Route(route)case "POST": // Create a resource.var handler restful.RouteFunctionif isNamedCreater {handler = CreateNamedResource(namedCreater, reqScope, a.group.Typer, admit)} else {handler = CreateResource(creater, reqScope, a.group.Typer, admit)}doc := "create a " + kindif hasSubresource {doc = "create " + subresource + " of a " + kind}route := ws.POST(action.Path).To(handler).Filter(m).Doc(doc).Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).Operation("create"+namespaced+kind+strings.Title(subresource)).Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).Returns(http.StatusOK, "OK", versionedObject).Reads(versionedObject).Writes(versionedObject)addParams(route, action.Params)ws.Route(route)case "DELETE": // Delete a resource.doc := "delete a " + kindif hasSubresource {doc = "delete " + subresource + " of a " + kind}route := ws.DELETE(action.Path).To(DeleteResource(gracefulDeleter, isGracefulDeleter, reqScope, admit)).Filter(m).Doc(doc).Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).Operation("delete"+namespaced+kind+strings.Title(subresource)).Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).Writes(versionedStatus).Returns(http.StatusOK, "OK", versionedStatus)if isGracefulDeleter {route.Reads(versionedDeleterObject)}addParams(route, action.Params)ws.Route(route)// TODO: deprecatedcase "WATCH": // Watch a resource.doc := "watch changes to an object of kind " + kindif hasSubresource {doc = "watch changes to " + subresource + " of an object of kind " + kind}route := ws.GET(action.Path).To(ListResource(lister, watcher, reqScope, true, a.minRequestTimeout)).Filter(m).Doc(doc).Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).Operation("watch"+namespaced+kind+strings.Title(subresource)).Produces("application/json").Returns(http.StatusOK, "OK", watchjson.WatchEvent{}).Writes(watchjson.WatchEvent{})if err := addObjectParams(ws, route, versionedListOptions); err != nil {return nil, err}addParams(route, action.Params)ws.Route(route)// TODO: deprecatedcase "WATCHLIST": // Watch all resources of a kind.doc := "watch individual changes to a list of " + kindif hasSubresource {doc = "watch individual changes to a list of " + subresource + " of " + kind}route := ws.GET(action.Path).To(ListResource(lister, watcher, reqScope, true, a.minRequestTimeout)).Filter(m).Doc(doc).Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).Operation("watch"+namespaced+kind+strings.Title(subresource)+"List").Produces("application/json").Returns(http.StatusOK, "OK", watchjson.WatchEvent{}).Writes(watchjson.WatchEvent{})if err := addObjectParams(ws, route, versionedListOptions); err != nil {return nil, err}addParams(route, action.Params)ws.Route(route)case "PROXY": // Proxy requests to a resource.// Accept all methods as per http://issue.k8s.io/3996addProxyRoute(ws, "GET", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params)addProxyRoute(ws, "PUT", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params)addProxyRoute(ws, "POST", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params)addProxyRoute(ws, "DELETE", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params)addProxyRoute(ws, "HEAD", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params)addProxyRoute(ws, "OPTIONS", a.prefix, action.Path, proxyHandler, namespaced, kind, resource, subresource, hasSubresource, action.Params)case "CONNECT":for _, method := range connecter.ConnectMethods() {doc := "connect " + method + " requests to " + kindif hasSubresource {doc = "connect " + method + " requests to " + subresource + " of " + kind}route := ws.Method(method).Path(action.Path).To(ConnectResource(connecter, reqScope, admit, connectOptionsKind, path, connectSubpath, connectSubpathKey)).Filter(m).Doc(doc).Operation("connect" + strings.Title(strings.ToLower(method)) + namespaced + kind + strings.Title(subresource)).Produces("*/*").Consumes("*/*").Writes("string")if versionedConnectOptions != nil {if err := addObjectParams(ws, route, versionedConnectOptions); err != nil {return nil, err}}addParams(route, action.Params)ws.Route(route)}default:return nil, fmt.Errorf("unrecognized action verb: %s", action.Verb)}// Note: update GetAttribs() when adding a custom handler.}return &apiResource, nil
}
从路由的注册流程来看,实现还是相对复杂,但是最终都是根据匹配后的路由信息,调用传入的storage的对应的创建,删除等方法。
etcd的操作流程
根据上文中传入的storage类型,分解的相关的方法,就是调用在初始化过程中生成的实例。例如像endpoint对应的storage代码如下。
// NewREST returns a RESTStorage object that will work against endpoints.
func NewREST(s storage.Interface, useCacher bool) *REST {prefix := "/services/endpoints"storageInterface := sif useCacher {config := storage.CacherConfig{CacheCapacity: 1000,Storage: s,Type: &api.Endpoints{},ResourcePrefix: prefix,KeyFunc: func(obj runtime.Object) (string, error) {return storage.NamespaceKeyFunc(prefix, obj)},NewListFunc: func() runtime.Object { return &api.EndpointsList{} },}storageInterface = storage.NewCacher(config)}store := &etcdgeneric.Etcd{ // 生成etcd的对应的方法NewFunc: func() runtime.Object { return &api.Endpoints{} },NewListFunc: func() runtime.Object { return &api.EndpointsList{} },KeyRootFunc: func(ctx api.Context) string {return etcdgeneric.NamespaceKeyRootFunc(ctx, prefix)},KeyFunc: func(ctx api.Context, name string) (string, error) {return etcdgeneric.NamespaceKeyFunc(ctx, prefix, name)},ObjectNameFunc: func(obj runtime.Object) (string, error) {return obj.(*api.Endpoints).Name, nil},PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher {return endpoint.MatchEndpoints(label, field)},EndpointName: "endpoints", // 设置endpoints前缀CreateStrategy: endpoint.Strategy, // 设置更新策略和更新策略UpdateStrategy: endpoint.Strategy,Storage: storageInterface, // 设置保存的接口}return &REST{store}
}...// Create inserts a new item according to the unique key from the object.
func (e *Etcd) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) {trace := util.NewTrace("Create " + reflect.TypeOf(obj).String())defer trace.LogIfLong(time.Second)if err := rest.BeforeCreate(e.CreateStrategy, ctx, obj); err != nil {return nil, err}name, err := e.ObjectNameFunc(obj) // 获取对象名称if err != nil {return nil, err}key, err := e.KeyFunc(ctx, name) // 调用key的类型if err != nil {return nil, err}ttl, err := e.calculateTTL(obj, 0, false)if err != nil {return nil, err}trace.Step("About to create object")out := e.NewFunc() if err := e.Storage.Create(key, obj, out, ttl); err != nil { // 创建数据err = etcderr.InterpretCreateError(err, e.EndpointName, name)err = rest.CheckGeneratedNameError(e.CreateStrategy, err, obj)return nil, err}trace.Step("Object created")if e.AfterCreate != nil {if err := e.AfterCreate(out); err != nil { // 检查是否有回调函数如果有则执行return nil, err}}if e.Decorator != nil {if err := e.Decorator(obj); err != nil {return nil, err}}return out, nil
}...// Get retrieves the item from etcd.
func (e *Etcd) Get(ctx api.Context, name string) (runtime.Object, error) {obj := e.NewFunc()trace := util.NewTrace("Get " + reflect.TypeOf(obj).String()) defer trace.LogIfLong(time.Second)key, err := e.KeyFunc(ctx, name) // 获取key的执行方法if err != nil {return nil, err}trace.Step("About to read object")if err := e.Storage.Get(key, obj, false); err != nil { // 查询key的内容return nil, etcderr.InterpretGetError(err, e.EndpointName, name)}trace.Step("Object read")if e.Decorator != nil {if err := e.Decorator(obj); err != nil {return nil, err}}return obj, nil
}
通过路由到存储的过程中,大概的过程就是如上所示,整个的代码的流程相对比较繁琐,不过实现起来确实非常的优秀。
总结
通过前几篇的k8s的基础概念的学习,本文的apiserver的实现的主要逻辑相对比较简单,但是实现起来比较有扩展性,故代码的设计上面相对繁多,即最核心的流程就是将API上对应的操作逻辑映射到etcd中,并实现一定的业务逻辑。由于本人才疏学浅,如有错误请批评指正。
k8s概念入门之apiserver-针对1.1.版本阅读相关推荐
- k8s概念入门之control-manager-针对1.1.版本阅读
control-manager 资源控制器主要是为了控制各种资源的变更信息,例如pod的创建新增,副本控制器和账户控制器等信息,资源控制器的主要职责就是通过list-watch机制,从APIServe ...
- k8s概念入门之kube-proxy-针对1.1版本阅读
背景 在后续阅读k8s0.4版本的过程中,发现文档上描述的确实是一个不完整的版本,故切换版本到1.1,因为在1.1文档中已经标明了可以在生成环境中使用,故重新再学习一下有关kube-proxy的内容, ...
- k8s概念入门之kubelet-针对1.1.版本阅读
kubelet kubelet是在每个节点上运行的主要"节点代理".它可以使用以下之一向apiserver注册该节点:主机名:用于覆盖主机名的标志:或云提供商的特定逻辑. kube ...
- k8s概念入门之kube-proxy-针对早期(0.4)版本阅读
k8s的kube-proxy分析 Kube-proxy主要是伴随着kubtlet进程一起部署在每个node节点中,proxy的功能主要就是为了完成在k8s集群中实现集群内部的通信,也可完成集群外的数据 ...
- Kubernetes ~ k8s 从入门到入坑。
Kubernetes ~ k8s 从入门到入坑. 文章目录 Kubernetes ~ k8s 从入门到入坑. 1. Kubernetes 介绍. 1.1 应用部署方式演变. 1.2 kubernete ...
- Kubernetes(k8s)入门及集群部署文档
文章目录 一.k8s 快速入门 简介 部署方式的进化 k8s能做什么 架构 整体主从方式 Master 节点架构 Node 节点架构 概念 快速体验 流程叙述 二.k8s 集群安装 前置要求 了解ku ...
- 【机器学习】机器学习和深度学习概念入门
机器学习和深度学习概念入门(上) 作者:谭东 来源:机器学习算法与自然语言处理 目 录 1 人工智能.机器学习.深度学习三者关系 2 什么是人工智能 3 什么是机器学习 4 机器学习之 ...
- 19年8月 字母哥 第一章 spring boot 2.x基础及概念入门 这里全部看完了 热部署没出来 第二章在前面2页 用热点公司网不行
http://springboot.zimug.com/1233100 文档 http://www.zimug.com/page/5 字母哥个人博客 11111 第一章 spring bo ...
- k8s dashboard_【大强哥-k8s从入门到放弃02】Kubernetes1.17部署Dashboard2.0
号外号外,后面所有提升视频都会更新到知乎和B站上去,不会直接发群里了,哈哈,能看懂这句话的我都认识,大家可以先关注一下,我知乎上的所有文档也会录成视频 更多视频详见 杨哥天云:https://spac ...
最新文章
- c语言文件操作中换行,关于文件操作,碰到空格就换行
- Windows常用快捷键汇总
- DevOps,到底是开发还是运维?
- linux 二进制差分工具,打造Android万能的软件更新库
- 洛谷P1312 Mayan游戏
- java实时解析mysql日志,利用maxwell 组件实时监听Mysql的Binlog日志,并且把解析的json格式数据发送到kafka窗口供实时消费...
- 基于SSM的汽车租赁系统
- Mac外接显示器问题解决
- [ 云原生之谜 ] 云原生背景 定义 相关技术详解?
- Post请求body为list,校验里面的对象
- 阿里云大数据之MaxComputer简介
- 超星阅读器ActiveX缓冲区溢出漏洞利用-LoadPage
- 方舟官方服务器怎么不显示伤害,方舟单人怎么显示伤害 | 手游网游页游攻略大全...
- 关于mysql的密码
- 合同信息管理系统(vue+elementUI+node.js)
- python画k线图_小白学Python(14)——pyecharts 绘制K线图 Kline/Candlestick
- 【开源代码】Wheel-SLAM:首个只有一个 Wheel-IMU 使用地形特征(由 Wheel-IMU 测量)的 SLAM 系统
- .html是什么方法,关于html:HTTP方法的默认形式是什么?
- 运维知识体系%%%谨以此文看下自己的处境
- eladmin前端 学习笔记
热门文章
- 2021 IDEA大会开启AI思想盛宴,用“创业精神”做科研
- 阿里云PolarDB数据库将云原生进行到底!业内首次实现三层池化
- 计算机视觉怎样实现自我超越?更大规模更精准的数据
- 360金融首席科学家张家兴:我们如何做数据AI融合中台?
- AI换脸、声音篡改等,明确写入新版《民法典》
- 为了一窥国足输韩国之后人们的评论,我爬了懂球帝App
- 百度发布智能小程序:“开放+AI”是最大特色
- 李飞飞团队最新论文:如何对图像中的实体精准“配对”?
- 这是2018年的第一场AI生态论坛,比以往来得更早一些
- AI一分钟 | 万达网科裁员95%高达5000余人,被爆下一步将转型AI; 英伟达放话了:研究人员放心用,不更新驱动就没啥事儿