热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

多厂商容器平台开发系统性总结

总述自2021年6月21号由玩云原生的运维转玩云原生的开发至今,已有5月有余,除去中间的一些其他工作任务,实际参与(实际是一个人负责开发)多厂商容器平台开发应有3月有余。个人开发并

总述

自2021年6月21号由玩云原生的运维转玩云原生的开发至今,已有5月有余,除去中间的一些其他工作任务,实际参与(实际是一个人负责开发)多厂商容器平台开发应有3月有余。个人开发并规划的多厂商容器平台是根据此张由我个人设计的规划图进行的(ps:部门没有架构师级别的,能提供可行的架构图或者一点指导,所以只能根据4年的kubernetes使用经验和逛各大网站和大厂的相关文档)。

也算是我心目中的多厂商容器平台的开发思路吧,下面将重点围绕上图展开。

注:此篇为我心目中的"多厂商容器管理平台"。

依赖项

主要

  • golang 1.16.6
  • goland 2021.2.3
  • gin 1.7.4
  • gorm 1.9.16
  • kubernetes 1.16 -- 1.19
  • client-go 0.19.0

规划图浅解

采取模块分层开发:以适配层为分界线

1、上层为适配层,根据不同的厂商进行API封装,统一返回字段,需要根据厂商的字段进行不同的方法开发。

2、下层为核心层,直接调kubernetes接口,统一返回字段。

各容器厂商会根据实际需求,对容器平台进行适当的整改,如:

集群的创建方案[1、在创建集群的同时按照参数创建云厂商节点资源 2、导入已有的节点资源创建集群 3、等等等]

节点特性[1、根据地域就近调度 2、控制节点是否可见 3、等等等]

kubernetes附件组建[1、网络插件的按需选择或二开 2、节点的pod最大数 3、等等等]

导入第三方kubernetes集群的方案[1、导入config 2、导入secret 3、导入agent 4、等等等]

......

根据以上的考虑:

1、集群和节点的增删改采取适配器的方案开发

2、集群和节点的查询走kubernetes,其一查询是个频繁的操作而厂家平台是有APi调用次数限制,其二集群和节点的状态应以kubernetes原生为依据而不应该是以厂家的容器平台为依据,其三减少代码的无用重复便于维护

3、每个厂家的仓库(helm仓库和image仓库)都有各自的特性且不属于kubernetes核心资源且无法像kubernetes那样获取到最底层的API文档,所以增删改查都走厂家似乎没有不妥之处。

4、数据库需要维护的有:集群信息、节点信息、仓库的有关权控信息。尽可能的减少此服务的维护复杂度,能交给厂家和kubernetes的etcd维护的最好。
4.1 如没有"集群没创建成功即可查看集群的个别信息和集群没创建成功既可查看节点的有关信息"的需求,那其实集群和节点也没有数据库维护的必要性,我认为。

5、kubernetes的核心资源,即可以被kubectl api-resources 查到的资源,通过kubernetes的api即可。

6、因为服务在可预见的时期是部署与虚机上,所以暂没有准备进行kubernetes 聚合api开发,但api最好为声明式的,留下可行性。

7、requests和response采取常规的json格式,没有选择采用yaml的json格式,主要是为了减轻前端的不便,且当下大厂也是各有格式,最主要的是目前还没有进行api聚合到kubernetes。

8、认为开发的重点应该在中期规划部分,这才所有创新,有所不同于众多厂家。

部分核心代码

目录结构:

.
├── Makefile
├── README.md
├── cmd
│   └── container
│       └── container.go
├── config
│   └── container.yaml
├── deploy
│   ├── docker
│   │   └── Dockerfile
│   └── vmware
├── docs
├── go.mod
├── go.sum
├── internal
│   └── container
│       ├── bootstrap
│       ├── controller
│       ├── dao
│       ├── dto
│       ├── ecode
│       ├── initialize
│       ├── job
│       ├── middleware
│       ├── models
│       ├── pkg
│       │   ├── kubernetes
│       │   │   ├── dto
│       │   │   └── service
│       │   ├── registry
│       │   │   ├── registry.go
│       │   │   └── tke
│       │   │   └── acr
│       │   │   └── harbor
│       │   └── rancher
│       │       ├── dto
│       │       └── service
│       │   └── ack
│       │       ├── dto
│       │       └── service
│       │   └── tke
│       │       ├── dto
│       │       └── service
│       ├── routers
│       └── service
├── pkg
├── scripts
│   ├── db.sh
│   └── deploy.sh
└── test

封装httpClient

type HTTPClient interface {
	Do(req *http.Request) (*http.Response, error)
}

var (
	Client HTTPClient
)

func init() {
	Client = &http.Client{
		Timeout: 10 * time.Second,
		Transport: &http.Transport{
			TLSClientConfig:   &tls.Config{InsecureSkipVerify: true},
			DisableKeepAlives: true,
			Proxy:             http.ProxyFromEnvironment,
			DialContext: (&net.Dialer{
				Timeout:   30 * time.Second, // tcp连接超时时间
				KeepAlive: 60 * time.Second, // 保持长连接的时间
				DualStack: true,
			}).DialContext, // 设置连接的参数
			MaxIdleConns:          50, // 最大空闲连接
			MaxConnsPerHost:       100,
			MaxIdleConnsPerHost:   100,              // 每个host保持的空闲连接数
			ExpectContinueTimeout: 30 * time.Second, // 等待服务第一响应的超时时间
			IdleConnTimeout:       60 * time.Second, // 空闲连接的超时时间
		},
	}
}

// CheckRespStatus 状态检查
func CheckRespStatus(resp *http.Response) ([]byte, error) {
	bodyBytes, _ := ioutil.ReadAll(resp.Body)
	if resp.StatusCode >= 200 && resp.StatusCode <400 {
		return bodyBytes, nil
	}
	return nil, errors.New(string(bodyBytes))
}

// Request 建立http请求
func Request(url, token, body string, headerSet map[string]string, method string) (respStatusCode int, respBytes []byte, err error) {
	request, err := http.NewRequest(method, url, strings.NewReader(body))
	if err != nil {
		return 401, nil, err
	}

	//添加token
	if token != "" {
		request.Header.Set("Authorization", "Bearer "+token)
	}

	// header 添加字段
	if headerSet != nil {
		for k, v := range headerSet {
			request.Header.Set(k, v)
		}
	}
	resp, err := Client.Do(request)
	if err != nil {
		return 401, nil, err
	}
	defer resp.Body.Close()
	// 返回的状态码
	respBytes, err = CheckRespStatus(resp)
	respStatusCode = resp.StatusCode
	return
}

封装clusterManager

type ClusterManager struct {
	ClientSet     *kubernetes.Clientset
	Metrics       *metrics.Clientset
	DynamicClient dynamic.Interface
}

const (
	//DefaultQPS High enough QPS to fit all expected use cases.
	DefaultQPS = 1e6
	//DefaultBurst High enough Burst to fit all expected use cases.
	DefaultBurst = 1e6
)

func buildConfig(clusterName string) (*rest.Config, error) {
	var clientConfig *rest.Config
	var configV1 *clientcmdapiv1.Config
	var dbCluster models.Cluster
	var err error
	var host string
	rows := bootstrap.DB.Where("cluster_name = ?", clusterName).Find(&dbCluster).RowsAffected
	if rows == 0 {
		return nil, errors.New("the database does not have this information")
	}
	if dbCluster.KubeConfigSecret != "" {
		kubeConfigBytes, err := base64.StdEncoding.DecodeString(dbCluster.KubeConfigSecret)
		kubeConfigJson, err := yaml.YAMLToJSON(kubeConfigBytes)
		err = json.Unmarshal(kubeConfigJson, &configV1)
		if err != nil {
			logrus.Error(err.Error())
		}
		// 切换匹配的版本
		configObject, err := clientcmdlatest.Scheme.ConvertToVersion(configV1, clientcmdapi.SchemeGroupVersion)
		configInternal := configObject.(*clientcmdapi.Config)
		// 实例化配置信息
		clientConfig, err = clientcmd.NewDefaultClientConfig(*configInternal, &clientcmd.ConfigOverrides{}).ClientConfig()
		clientConfig.QPS = DefaultQPS
		clientConfig.Burst = DefaultBurst
	} else if dbCluster.Token != "" {
		var addresses []dto.Addresses
		err := json.Unmarshal([]byte(dbCluster.APIServer), &addresses)
		for _, address := range addresses {
			if address.Type == "Real" {
				host = fmt.Sprintf("https://") + address.Host + fmt.Sprintf(":") + strconv.Itoa(address.Port)
				break
			}
		}
		if err != nil {
			return nil, errors.New("request connection cluster failed")
		}

		clientCOnfig= &rest.Config{
			Host:                host,
			APIPath:             "",
			ContentConfig:       rest.ContentConfig{},
			Username:            "",
			Password:            "",
			BearerToken:         dbCluster.Token,
			BearerTokenFile:     "",
			Impersonate:         rest.ImpersonationConfig{},
			AuthProvider:        nil,
			AuthConfigPersister: nil,
			ExecProvider:        nil,
			TLSClientConfig: rest.TLSClientConfig{
				Insecure: true,
			},
			UserAgent:          "",
			DisableCompression: false,
			Transport:          nil,
			WrapTransport:      nil,
			QPS:                DefaultQPS,
			Burst:              DefaultBurst,
			RateLimiter:        nil,
			WarningHandler:     nil,
			Timeout:            0,
			Dial:               nil,
			Proxy:              nil,
		}
	} else {
		return nil, errors.New("build  client config error")
	}
	return clientConfig, nil
}

func BuildApiServerClient(clusterName string) (*ClusterManager, error) {
	clientConfig, err := buildConfig(clusterName)
	if err != nil {
		return nil, err
	}
	if clientCOnfig== nil {
		return nil, errors.New("err: error BuildApiServerClient")
	}
	clientSet, err := kubernetes.NewForConfig(clientConfig)
	if err != nil {
		return nil, err
	}
	// 这里一定要调用Discovery().ServerVersion(),探测Kube apiServer是否可用,因为kubernetes.NewForConfig(restConfig)不会去检查服务是否可用,当服务不可用时,该方法不会返回错误的
	_, err = clientSet.Discovery().ServerVersion()
	if err != nil {
		return nil, err
	}
	m, err := metrics.NewForConfig(clientConfig)
	if err != nil {
		return nil, err
	}
	d, err := dynamic.NewForConfig(clientConfig)
	if err != nil {
		return nil, err
	}
	clusterManager := &ClusterManager{
		clientSet,
		m,
		d,
	}
	return clusterManager, nil
}

封装reponseApi

type ApiResponse struct {
	Code int         `json:"code"`
	Msg  string      `json:"message"`
	Data interface{} `json:"data"`
}

// PaginateResponse 显然这个结构体可以复用 ApiResponse, 但是 swagger 不认识!
type PaginateResponse struct {
	Code int      `json:"code"`
	Msg  string   `json:"message"`
	Data Paginate `json:"data"`
}

type Paginate struct {
	CurPage     int         `json:"cur_page"`      // 当前页
	CurPageSize int         `json:"cur_page_size"` // 每页展示数据量
	Total       int         `json:"total"`         // 总共数据量
	TotalPage   int         `json:"total_page"`    // 总共页数
	Data        interface{} `json:"data"`          // 数据
}

// SuccessResponse API成功返回
func SuccessResponse(c *gin.Context, data interface{}) {
	response(c, ecode.Success, data)
}

// SuccessPaginateResponse 分页返回
func SuccessPaginateResponse(c *gin.Context, data interface{}, total int, curPage int, curPageSize int) {
	c.JSON(http.StatusOK, PaginateResponse{
		Code: int(ecode.Success),
		Msg:  ecode.ErrMsg[ecode.Success],
		Data: Paginate{CurPage: curPage, CurPageSize: curPageSize, Total: total, TotalPage: int(math.Ceil(float64(total) / float64(curPageSize))), Data: data},
	})
	c.Abort()
}

// ErrorResponse API失败返回
func ErrorResponse(c *gin.Context, code ecode.ErrCode, data interface{}) {
	response(c, code, data)
}

func NotFoundResponse(c *gin.Context) {
	c.JSON(http.StatusNotFound, gin.H{
		"code":    404,
		"message": "页面未找到",
		"data":    "",
	})
}

func response(c *gin.Context, code ecode.ErrCode, data interface{}) {
	c.JSON(http.StatusOK, ApiResponse{
		Code: int(code),
		Msg:  ecode.ErrMsg[code],
		Data: data,
	})
	c.Abort()
}

几个调kubernetes的例子【认为比较有趣的例子】

# 倒序输出事件
	events, err := w.workloadKubernetes.FindEvents(clientSet, namespace, name)
	if err != nil {
		return nil, err
	}
	var eventsWorkloadReps []*dto.EventsWorkloadRep
	t := time.Time{}
	for i := len(events) - 1; i >= 0; i-- {
		if events[i].FirstTimestamp.Time == t {
			events[i].FirstTimestamp.Time = events[i].EventTime.Time
		}
		if events[i].LastTimestamp.Time == t {
			events[i].LastTimestamp.Time = events[i].EventTime.Time
		}
		eventsWorkloadReps = append(eventsWorkloadReps, &dto.EventsWorkloadRep{
			WorkloadUUID:   id,
			FirstTimestamp: events[i].FirstTimestamp.Time,
			LastTimestamp:  events[i].LastTimestamp.Time,
			Type:           events[i].Type,
			Kind:           events[i].InvolvedObject.Kind,
			Name:           events[i].Name,
			Reason:         events[i].Reason,
			Message:        events[i].Message,
			Count:          events[i].Count,
		})
	}
	if len(eventsWorkloadReps) == 0 {
		return make([]*dto.EventsWorkloadRep, 0), nil
	}
	return eventsWorkloadReps, nil

# pod log
    limit, _ := strconv.ParseInt(tailLines, 10, 64)
    req := clientSet.CoreV1().Pods(namespace).GetLogs(name, &coreV1.PodLogOptions{Container: container, Timestamps: true, TailLines: &limit})
    podLogs, err := req.Stream(context.TODO())
    if err != nil {
    return "error in opening stream"
    }
    defer podLogs.Close()
    
    buf := new(bytes.Buffer)
    _, err = io.Copy(buf, podLogs)
    if err != nil {
        return "error in copy information from podLogs to buf"
    }
    str := buf.String()
    return str

# 根据kubeconfig 获取ApiServer\CertFile\Token
    decoded, err := base64.StdEncoding.DecodeString(kubeConfig)
    decodestr := string(decoded)
    // 认证方式为kubeConfig
    // 通过kubeConfig获取 api / token / certFile
    kubeConfigJson, err := syaml.YAMLToJSON([]byte(decodestr))
    var configV1 *clientcmdapiv1.Config
    err = json.Unmarshal(kubeConfigJson, &configV1)
    if err != nil {
    return nil, nil, err
    }
    c, err := clientcmd.RESTConfigFromKubeConfig(decoded)
    if err != nil {
    return nil, nil, err
    }
    clientSet, err := kubernetes.NewForConfig(c)
    if err != nil {
    return nil, nil, err
    }
    sa, err := clientSet.CoreV1().ServiceAccounts("kube-system").Get(context.TODO(), "admin-user", metaV1.GetOptions{})
    secrets, err := clientSet.CoreV1().Secrets("kube-system").Get(context.TODO(), sa.Secrets[0].Name, metaV1.GetOptions{})
    if err != nil {
			return nil, err
    }
    importClusterReq.ApiServer = configV1.Clusters[0].Cluster.Server
    encoded := base64.StdEncoding.EncodeToString(configV1.Clusters[0].Cluster.CertificateAuthorityData)
    importClusterReq.CertFile = encoded
    importClusterReq.Token = string(secrets.Data["token"])

# 实现apply yaml 【......写成了x了】
func (y *Yaml) ApplyYaml(dynamicClient dynamic.Interface, clientSet *kubernetes.Clientset, yamlBody []byte) (interface{}, error) {
    data, err := yamlutil.ToJSON(yamlBody)
    var applyYaml dto.ApplyYaml
    if err = json.Unmarshal(data, &applyYaml); err != nil {
        return nil, err
    }
    var applyYamlRep string
    decoder := yamlutil.NewYAMLOrJSONDecoder(bytes.NewReader(yamlBody), len(yamlBody))
    var rawObj runtime.RawExtension
    if err := decoder.Decode(&rawObj); err != nil {
        return nil, err
    }
    obj, gvk, err := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme).Decode(rawObj.Raw, nil, nil)
    unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
    if err != nil {
    return nil, err
    }

	unstructuredObj := &unstructured.Unstructured{Object: unstructuredMap}
	// 获取支持的资源类型列表
	gr, err := restmapper.GetAPIGroupResources(clientSet.Discovery())
	if err != nil {
		return nil, err
	}

	// 创建 'Discovery REST Mapper',获取查询的资源的类型
	mapper := restmapper.NewDiscoveryRESTMapper(gr)
	// 查找 Group/Version/Kind 的 REST 映射
	mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
	if err != nil {
		return nil, err
	}

	var dri dynamic.ResourceInterface
	// 需要为 namespace 范围内的资源提供不同的接口
	if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
		if unstructuredObj.GetNamespace() == "" {
			unstructuredObj.SetNamespace("default")
		}
		dri = dynamicClient.Resource(mapping.Resource).Namespace(unstructuredObj.GetNamespace())
	} else {
		dri = dynamicClient.Resource(mapping.Resource)
	}

	if applyYaml.Metadata.Namespace == "" {
		applyYaml.Metadata.Namespace = "default"
	}
	// 查询k8s是否有该资源类型
	switch applyYaml.Kind {
	case "Deployment":
		_, err = clientSet.AppsV1().Deployments(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "StatefulSet":
		_, err = clientSet.AppsV1().StatefulSets(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "DaemonSet":
		_, err = clientSet.AppsV1().DaemonSets(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "ReplicaSet":
		_, err = clientSet.AppsV1().ReplicaSets(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "CronJob":
		_, err = clientSet.BatchV1().CronJobs(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "Job":
		_, err = clientSet.BatchV1().Jobs(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "Service":
		_, err = clientSet.CoreV1().Services(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "ConfigMap":
		_, err = clientSet.CoreV1().ConfigMaps(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "Ingress":
		_, err = clientSet.ExtensionsV1beta1().Ingresses(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "ServiceAccount":
		_, err = clientSet.CoreV1().ServiceAccounts(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "ClusterRole":
		_, err = clientSet.RbacV1().ClusterRoles().Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "RoleBinding":
		_, err = clientSet.RbacV1().RoleBindings(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "ClusterRoleBinding":
		_, err = clientSet.RbacV1().ClusterRoleBindings().Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "APIService":
		_, err := dri.Create(context.Background(), unstructuredObj, metaV1.CreateOptions{})
		if err != nil {
			return nil, err
		}
	}
	if err != nil {
		if !errors.IsNotFound(err) {
			return nil, err
		}
		// 不存在则创建
		obj2, err := dri.Create(context.Background(), unstructuredObj, metaV1.CreateOptions{})
		fmt.Println("obj2", obj2)
		if err != nil {
			return nil, err
		}
		applyYamlRep = fmt.Sprintf("%s/%s/%s created", obj2.GetNamespace(), obj2.GetKind(), obj2.GetName())
	} else { // 已存在则更新
		obj2, err := dri.Update(context.Background(), unstructuredObj, metaV1.UpdateOptions{})
		if err != nil {
			return nil, err
		}
		applyYamlRep = fmt.Sprintf("%s/%s/%s update", obj2.GetNamespace(), obj2.GetKind(), obj2.GetName())
	}
	return applyYamlRep, nil
}

以registry的命名空间新建为例 来个适配器的demo 【伪代码】

# controller层
func CreateContainerRegistryNamespace(c *gin.Context) {
    var param dto.CreateContainerRegistryNamespaceReq
    if err := c.ShouldBindJSON(¶m); err != nil {
        controller.ErrorResponse(c, ecode.PARAMETER_ERR, err.Error())
        return
    }
    cred, err := service.NewCredentialService().GetPlainTextCredential(param.CredentialID)
    if err != nil {
        ErrorResponse(c, ecode.ParameterErr, err.Error())
        return
    }
    param.Token = cred.Token
    if data, err := service.NewRegistryService(param.Source,param.Token).CreateContainerRegistryNamespace(param); err != nil {
        controller.ErrorResponse(c, ecode.PARAMETER_ERR, err.Error())
    } else {
        controller.SuccessResponse(c, data)
    }
}

# service层
type registryService struct {
    cli         registry.Registry
    registryDao dao.RegistryDao
}

func NewRegistryService(source,token string) *registryService {
    return ®istryService{
        cli:         registry.NewRegistryCli(source,token),
    }
}
func (r *registryService) CreateContainerRegistryNamespace(param dto.CreateContainerRegistryNamespaceReq) (interface{}, error) {
    rep, err := r.cli.CreateContainerRegistryNamespace(param.DisplayName, param.Describe, param.Visibility)
    return rep, err
}

# interface层
type Registry interface {
    CreateContainerRegistryNamespace(name, describe, visibility, uuid string) (*dto.ListContainerRegistryNamespacesRep, error)
}

func NewRegistryCli(source,token string) Registry {
    var cli Registry
    switch source {
    case "tke":
        cli = tke.NewTkeClient(token)
    }
    return cli
}

# 方法层
type tkeClient struct {
    token string 
}

func NewTkeClient(token) *tkeClient {
    return &tkeClient{
        token: token
    }
}

func (t *tkeClient) CreateContainerRegistryNamespace(name, describe, visibility, uuid string) (*dto.ListContainerRegistryNamespacesRep, error) {
    url := viper.GetString("url") + "/apis/registry.tkestack.io/v1/namespaces/"
    createJson := tDto.CreateContainerRegistryReq{
        APIVersion: "registry.tkestack.io/v1",
        Kind:       "Namespace",
        Spec: tDto.CreateContainerRegistryReqSpec{
            Name:        name,
            DisplayName: describe,
            Visibility:  visibility,
        },
    }
    jsonData, errs := json.Marshal(createJson)
    if errs != nil {
        return nil, errs
    }
    _, rep, err := pkg.Request(url, token, string(jsonData), nil, http.MethodPost)
    if err != nil {
        errRep := gojsonq.New().FromString(err.Error()).Find("message")
        return nil, errors.New(errRep.(string))
    }
    var listRep *tDto.ListRepItems
    if err = json.NewDecoder(strings.NewReader(string(rep))).Decode(&listRep); err != nil {
        errRep := gojsonq.New().FromString(err.Error()).Find("message")
        return nil, errors.New(errRep.(string))
    }
    var createRep dto.ListContainerRegistryNamespacesRep
    createRep.DisplayName = listRep.Spec.Name
    createRep.Describe = listRep.Spec.DisplayName
    createRep.Visibility = listRep.Spec.Visibility
    createRep.Name = listRep.Metadata.Name
    createRep.RepoCount = listRep.Status.RepoCount
    return &createRep, nil
}

注:由上所述 似乎并没有牵扯到多么高深的操作,甚至是单纯的api调用、封装,也未涉及到中间件类的应用,随着开发的不断深入,此服务应逐渐复杂化

参考链接:

kubernetes 源码分析:https://jeffdingzone.com/category/k8s/

kubernetes api文档:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/

图解kubernetes中API聚合机制的实现: https://juejin.cn/post/6844904081438277640

单体仓库与多仓库都有哪些优势劣势,如何确定微服务落地的最佳实践?:https://ssoor.github.io/2020/03/24/mono-repo-vs-multi-repo/

kubernetes Events介绍:https://www.kubernetes.org.cn/1031.html


推荐阅读
  • 2018年人工智能大数据的爆发,学Java还是Python?
    本文介绍了2018年人工智能大数据的爆发以及学习Java和Python的相关知识。在人工智能和大数据时代,Java和Python这两门编程语言都很优秀且火爆。选择学习哪门语言要根据个人兴趣爱好来决定。Python是一门拥有简洁语法的高级编程语言,容易上手。其特色之一是强制使用空白符作为语句缩进,使得新手可以快速上手。目前,Python在人工智能领域有着广泛的应用。如果对Java、Python或大数据感兴趣,欢迎加入qq群458345782。 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • Oracle分析函数first_value()和last_value()的用法及原理
    本文介绍了Oracle分析函数first_value()和last_value()的用法和原理,以及在查询销售记录日期和部门中的应用。通过示例和解释,详细说明了first_value()和last_value()的功能和不同之处。同时,对于last_value()的结果出现不一样的情况进行了解释,并提供了理解last_value()默认统计范围的方法。该文对于使用Oracle分析函数的开发人员和数据库管理员具有参考价值。 ... [详细]
  • 也就是|小窗_卷积的特征提取与参数计算
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了卷积的特征提取与参数计算相关的知识,希望对你有一定的参考价值。Dense和Conv2D根本区别在于,Den ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 如何查询zone下的表的信息
    本文介绍了如何通过TcaplusDB知识库查询zone下的表的信息。包括请求地址、GET请求参数说明、返回参数说明等内容。通过curl方法发起请求,并提供了请求示例。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文讨论了如何使用IF函数从基于有限输入列表的有限输出列表中获取输出,并提出了是否有更快/更有效的执行代码的方法。作者希望了解是否有办法缩短代码,并从自我开发的角度来看是否有更好的方法。提供的代码可以按原样工作,但作者想知道是否有更好的方法来执行这样的任务。 ... [详细]
  • 本文讨论了一个数列求和问题,该数列按照一定规律生成。通过观察数列的规律,我们可以得出求解该问题的算法。具体算法为计算前n项i*f[i]的和,其中f[i]表示数列中有i个数字。根据参考的思路,我们可以将算法的时间复杂度控制在O(n),即计算到5e5即可满足1e9的要求。 ... [详细]
  • This article discusses the efficiency of using char str[] and char *str and whether there is any reason to prefer one over the other. It explains the difference between the two and provides an example to illustrate their usage. ... [详细]
  • VSCode快速查看函数定义和代码追踪方法详解
    本文详细介绍了在VSCode中快速查看函数定义和代码追踪的方法,包括跳转到定义位置的三种方式和返回跳转前的位置的快捷键。同时,还介绍了代码追踪插件的使用以及对符号跳转的不足之处。文章指出,直接跳转到定义和实现的位置对于程序员来说非常重要,但需要语言本身的支持。以TypeScript为例,按下F12即可跳转到函数的定义处。 ... [详细]
  • 使用C++编写程序实现增加或删除桌面的右键列表项
    本文介绍了使用C++编写程序实现增加或删除桌面的右键列表项的方法。首先通过操作注册表来实现增加或删除右键列表项的目的,然后使用管理注册表的函数来编写程序。文章详细介绍了使用的五种函数:RegCreateKey、RegSetValueEx、RegOpenKeyEx、RegDeleteKey和RegCloseKey,并给出了增加一项的函数写法。通过本文的方法,可以方便地自定义桌面的右键列表项。 ... [详细]
author-avatar
缤纷之铃6868
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有