前言

序列化和反序列化在很多项目中都有应用,Kubernetes也不例外。Kubernetes中定义了大量的API对象,为此还单独设计了一个包,方便多个模块引用。API对象在不同的模块之间传输(尤其是跨进程)可能会用到序列化与反序列化,不同的场景对于序列化个格式又不同,比如grpc协议用protobuf,用户交互用yaml(因为yaml可读性强),etcd存储用json。Kubernetes反序列化API对象不同于我们常用的json.Unmarshal()函数(需要传入对象指针),Kubernetes需要解析对象的类型(Group/Version/Kind),根据API对象的类型构造API对象,然后再反序列化。因此,Kubernetes定义了Serializer接口,专门用于API对象的序列化和反序列化。

本文引用源码为kubernetes的release-1.21分支。

Serializer

因为Kubernetes需要支持json、yaml、protobuf三种数据格式的序列化和反序列化,有必要抽象序列化和反序列化的统一接口,源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/interfaces.go#L86

// Serializer是用于序列化和反序列化API对象的核心接口,
type Serializer interface {// Serializer继承了编码器和解码器,编码器就是用来序列化API对象的,序列化的过程称之为编码;反之,反序列化的过程称之为解码。// 关于编/解码器的定义下面有注释。EncoderDecoder
}// 序列化的过程称之为编码,实现编码的对象称之为编码器(Encoder)
type Encoder interface {// Encode()将对象写入流。可以将Encode()看做为json(yaml).Marshal(),只是输出变为io.Writer。Encode(obj Object, w io.Writer) error// Identifier()返回编码器的标识符,当且仅当两个不同的编码器编码同一个对象的输出是相同的,那么这两个编码器的标识符也应该是相同的。// 也就是说,编码器都有一个标识符,两个编码器的标识符可能是相同的,判断标准是编码任意API对象时输出都是相同的。// 标识符有什么用?标识符目标是与CacheableObject.CacheEncode()方法一起使用,CacheableObject又是什么东东?后面有介绍。Identifier() Identifier
}// 标识符就是字符串,可以简单的理解为标签的字符串形式,后面会看到如何生成标识符。
type Identifier string// 反序列化的过程称之为解码,实现解码的对象称之为解码器(Decoder)
type Decoder interface {// Decode()尝试使用Schema中注册的类型或者提供的默认的GVK反序列化API对象。// 如果'into'非空将被用作目标类型,接口实现可能会选择使用它而不是重新构造一个对象。// 但是不能保证输出到'into'指向的对象,因为返回的对象不保证匹配'into'。// 如果提供了默认GVK,将应用默认GVK反序列化,如果未提供默认GVK或仅提供部分,则使用'into'的类型补全。Decode(data []byte, defaults *schema.GroupVersionKind, into Object) (Object, *schema.GroupVersionKind, error)
}

我们平时工作中最常用的序列化格式包括json、yaml以及protobuf,非常"巧合",Kubernetes也是用这几种序列化格式。以json为例,编码器和解码器可以等同于json.Marshal()和json.Unmarshal(),定义成interface是对序列化与反序列化的统一抽象。为什么我们平时很少抽象(当然有些读者是有抽象的,我们不能一概而论),是因为我们工作中可能只用到一种序列化格式,所以抽象显得没那么必要。而Kubernetes中,这三种都是需要的,yaml的可视化效果好,比如我们写的各种yaml文件;而API对象存储在etcd中是json格式,在用到grpc的地方则需要protobuf格式。

再者,Kubernetes对于Serializer的定义有更高的要求,即根据序列化的数据中的元数据自动识别API对象的类型(GVK),这在Decoder.Decode()接口定义中已经有所了解。而我们平时使用json.Marshal()的时候传入了指定类型的对象指针,相比于Kubernetes对于反序列化的要求,我们使用的相对更"静态"。

综上所述,抽象Serializer就有必要了,尤其是RecognizingDecoder可以解码任意格式的API对象就可以充分体现这种抽象的价值。

json

json.Serializer实现了将API对象序列化成json数据和从json数据反序列化API对象,源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/serializer/json/json.go#L100

// Serializer实现了runtime.Serializer接口。
type Serializer struct {// MetaFactory从json数据中提取GVK(Group/Version/Kind),下面有MetaFactory注释。// MetaFactory很有用,解码时如果不提供默认的GVK和API对象指针,就要靠MetaFactory提取GVK了。// 当然,即便提供了供默认的GVK和API对象指针,提取的GVK的也是非常有用的,详情参看Decode()接口的实现。meta    MetaFactory// SerializerOptions是Serializer选项,可以看做是配置,下面有注释。options SerializerOptions// runtime.ObjectCreater根据GVK构造API对象,在反序列化时会用到,其实它就是Schema。// runtime.ObjectCreater的定义读者可以自己查看源码,如果对Schema熟悉的读者这都不是事。creater runtime.ObjectCreater// runtime.ObjectTyper根据API对象返回可能的GVK,也是用在反序列化中,其实它也是Schema。// 这个有什么用?runtime.Serializer.Decode()接口注释说的很清楚,在json数据和默认GVK无法提供的类型元数据需要用输出类型补全。typer   runtime.ObjectTyper// 标识符,Serializer一旦被创建,标识符就不会变了。identifier runtime.Identifier
}// SerializerOptions定义了Serializer的选项.
type SerializerOptions struct {// true: 序列化/反序列化yaml;false: 序列化/反序列化json// 也就是说,json.Serializer既可以序列化/反序列json,也可以序列化/反序列yaml。Yaml bool// Pretty选项仅用于Encode接口,输出易于阅读的json数据。当Yaml选项为true时,Pretty选项被忽略,因为yaml本身就易于阅读。// 什么是易于阅读的?举个例子就立刻明白了,定义测试类型为:// type Test struct {//     A int//     B string    // }// 则关闭和开启Pretty选项的对比如下:// {"A":1,"B":"2"}// {//   "A": 1,//   "B": "2"// }// 很明显,后者更易于阅读。易于阅读只有人看的时候才有需要,对于机器来说一点价值都没有,所以这个选项使用范围还是比较有限的。Pretty bool// Strict应用于Decode接口,表示严谨的。那什么是严谨的?笔者很难用语言表达,但是以下几种情况是不严谨的:// 1. 存在重复字段,比如{"value":1,"value":1};// 2. 不存在的字段,比如{"unknown": 1},而目标API对象中不存在Unknown属性;// 3. 未打标签字段,比如{"Other":"test"},虽然目标API对象中有Other字段,但是没有打`json:"Other"`标签// Strict选项可以理解为增加了很多校验,请注意,启用此选项的性能下降非常严重,因此不应在性能敏感的场景中使用。// 那什么场景需要用到Strict选项?比如Kubernetes各个服务的配置API,对性能要求不高,但需要严格的校验。Strict bool
}

MetaFactory

MetaFactory类型名定义其实挺忽悠人的,工厂类都是用来构造对象的,MetaFactory的功能虽然也是构造类型元数据的,但是它更像是一个解析器,所以笔者认为"MetaParser"更加贴切,源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/serializer/json/meta.go#L28

type MetaFactory interface {// 解析json数据中的元数据字段,返回GVK(Group/Version/Kind)。// 如果MetaFactory就这么一个接口函数,笔者认为叫解释器或者解析器更加合理。Interpret(data []byte) (*schema.GroupVersionKind, error)
}// SimpleMetaFactory是MetaFactory的一种实现,用于检索在json中由"apiVersion"和"kind"字段标识的对象的类型和版本。
type SimpleMetaFactory struct {
}// Interpret()实现了MetaFactory.Interpret()接口。
func (SimpleMetaFactory) Interpret(data []byte) (*schema.GroupVersionKind, error) {// 定义一种只有apiVersion和kind两个字段的匿名类型findKind := struct {// +optionalAPIVersion string `json:"apiVersion,omitempty"`// +optionalKind string `json:"kind,omitempty"`}{}// 只解析json中apiVersion和kind字段,这个玩法有点意思,但是笔者认为这个方法有点简单粗暴。// 读者可以尝试阅读json.Unmarshal(),该函数会遍历整个json,开销不小,其实必要性不强,因为只需要apiVersion和kind字段。// 试想一下,如果每次反序列化一个API对象都要有一次Interpret()和Decode(),它的开销相当于做了两次反序列化。if err := json.Unmarshal(data, &findKind); err != nil {return nil, fmt.Errorf("couldn't get version/kind; json parse error: %v", err)}// 将apiVersion解析为Group和Versiongv, err := schema.ParseGroupVersion(findKind.APIVersion)if err != nil {return nil, err}// 返回API对象的GVKreturn &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: findKind.Kind}, nil
}

Decode

其实Serializer的重头戏在解码,因为解码需要考虑的事情比较多,比如提取类型元数据(GVK),根据类型元数据构造API对象,当然还要考虑复用传入的API对象。而编码就没有这么复杂,所以理解了解码的实现,编码就基本可以忽略不计了。源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/serializer/json/json.go#L209

/ Decode实现了Decoder.Decode(),尝试从数据中提取的API类型(GVK),应用提供的默认GVK,然后将数据加载到所需类型或提供的'into'匹配的对象中:
// 1. 如果into为*runtime.Unknown,则将提取原始数据,并且不执行解码;
// 2. 如果into的类型没有在Schema注册,则使用json.Unmarshal()直接反序列化到'into'指向的对象中;
// 3. 如果'into'不为空且原始数据GVK不全,则'into'的类型(GVK)将用于补全GVK;
// 4. 如果'into'为空或数据中的GVK与'into'的GVK不同,它将使用ObjectCreater.New(gvk)生成一个新对象;
// 成功或大部分错误都会返回GVK,GVK的计算优先级为originalData > default gvk > into.
func (s *Serializer) Decode(originalData []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {data := originalData// 如果配置选项为yaml,则将yaml格式转为json格式,是不是有一种感觉:“卧了个槽”!所谓可支持json和yaml,就是先将yaml转为json!// 那我感觉我可以支持所有格式,都转为json就完了呗!其实不然,yaml的使用场景都是需要和人交互的地方,所以对于效率要求不高(qps低)。// 那么实现简单易于维护更重要,所以这种实现并没有什么毛病。if s.options.Yaml {// yaml转jsonaltered, err := yaml.YAMLToJSON(data)if err != nil {return nil, nil, err}data = altered}// 此时data是json了,以后就不用再考虑yaml选项了// 解析类型元数据,原理已经在MetaFactory解释过了。actual, err := s.meta.Interpret(data)if err != nil {return nil, nil, err}// 解析类型元数据大部分情况是正确的,除非不是json或者apiVersion格式不对。// 但是GVK三元组可能有所缺失,比如只有Kind,Group/Version,其他字段就用默认的GVK补全。// 这也体现出了原始数据中的GVK的优先级最高,其次是默认的GVK。gvkWithDefaults()函数下面有注释。if gvk != nil {*actual = gvkWithDefaults(*actual, *gvk)}// 如果'into'是*runtime.Unknown类型,需要返回*runtime.Unknown类型的对象。// 需要注意的是,此处复用了'into'指向的对象,因为返回的类型与'into'指向的类型完全匹配。if unk, ok := into.(*runtime.Unknown); ok && unk != nil {// 输出原始数据unk.Raw = originalData// 输出数据格式为JSON,那么问题来了,不是'data'才能保证是json么?如s.options.Yaml == true,originalData是yaml格式才对。// 所以笔者有必要提一个issue,看看官方怎么解决,如此明显的一个问题为什么没有暴露出来?// 笔者猜测:runtime.Unknown只在内部使用,而内部只用json格式,所以自然不会暴露出来。unk.ContentType = runtime.ContentTypeJSON// 输出GVK。unk.GetObjectKind().SetGroupVersionKind(*actual)return unk, actual, nil}// 'into'不为空,通过into类型提取GVK,这样在原始数据中的GVK和默认GVK都没有的字段用into的GVK补全。if into != nil {// 判断'into'是否为runtime.Unstructured类型_, isUnstructured := into.(runtime.Unstructured)// 获取'into'的GVK,需要注意的是返回的是一组GVK,types[0]是推荐的GVK。types, _, err := s.typer.ObjectKinds(into)switch {// 'into'的类型如果没有被注册或者为runtime.Unstructured类型,则直接反序列成'into'指向的对象。// 没有被注册的类型自然无法构造对象,而非结构体等同于map[string]interface{},不可能是API对象(因为API对象必须是结构体)。// 所以这两种情况直接反序列化到'into'对象就可以了,此时与json.Unmarshal()没什么区别。case runtime.IsNotRegisteredError(err), isUnstructured:if err := caseSensitiveJSONIterator.Unmarshal(data, into); err != nil {return nil, actual, err}return into, actual, nil// 获取'into'类型出错,多半是因为不是指针case err != nil:return nil, actual, err// 用'into'的GVK补全未设置的GVK,所以GVK的优先级:originalData > default gvk > intodefault:*actual = gvkWithDefaults(*actual, types[0])}}// 如果没有Kindif len(actual.Kind) == 0 {return nil, actual, runtime.NewMissingKindErr(string(originalData))}// 如果没有Versionif len(actual.Version) == 0 {return nil, actual, runtime.NewMissingVersionErr(string(originalData))}// 那么问题来了,为什么不判断Group?Group为""表示"core",比如我们写ymal的时候Kind为Pod,apiVersion是v1,并没有设置Group。// 从函数名字可以看出复用'into'或者重新构造对象,复用的原则是:如果'into'注册的一组GVK有任何一个与*actual相同,则复用'into'。// runtime.UseOrCreateObject()源码读者感兴趣可以自己看下obj, err := runtime.UseOrCreateObject(s.typer, s.creater, *actual, into)if err != nil {return nil, actual, err}// 反序列化对象,caseSensitiveJSONIterator暂且不用关心,此处可以理解为json.Unmarshal()。// 当然,读者非要知道个究竟,可以看看代码,笔者此处不注释。if err := caseSensitiveJSONIterator.Unmarshal(data, obj); err != nil {return nil, actual, err}// 如果是非strict模式,可以直接返回了。其实到此为止就可以了,后面是针对strict模式的代码,是否了解并不重要。if !s.options.Strict {return obj, actual, nil}// 笔者第一眼看到下面的以为看错了,但是擦了擦懵逼的双眼,发现就是YAMLToJSON。如果原始数据是json不会有问题么?// 笔者查看了一下yaml.YAMLToJSONStrict()函数注释:由于JSON是YAML的子集,因此通过此方法传递JSON应该是没有任何操作的。// 除非存在重复的字段,会解析出错。所以此处就是用来检测是否有重复字段的,当然,如果是yaml格式顺便转成了json。// 感兴趣的读者可以阅读源码,笔者只要知道它的功能就行了,就不“深究”了。altered, err := yaml.YAMLToJSONStrict(originalData)if err != nil {return nil, actual, runtime.NewStrictDecodingError(err.Error(), string(originalData))}// 接下来会因为未知的字段报错,比如对象未定义的字段,未打标签的字段等。// 此处使用DeepCopyObject()等同于新构造了一个对象,而这个对象其实又没什么用,仅作为一个临时的变量使用。strictObj := obj.DeepCopyObject()if err := strictCaseSensitiveJSONIterator.Unmarshal(altered, strictObj); err != nil {return nil, actual, runtime.NewStrictDecodingError(err.Error(), string(originalData))}// 返回反序列化的对象、GVK,所谓的strict模式无非是再做了一次转换和反序列化来校验数据的正确性,结果直接丢弃。// 所以说strict没有必要不用开启,除非你真正理解他的作用并且能够承受带来的后果。return obj, actual, nil
}// gvkWithDefaults()利用defaultGVK补全actual中未设置的字段。
// 需要注意的是,参数'defaultGVK'只是一次调用相对于actual的默认GVK,不是Serializer.Decode()的默认GVK。
func gvkWithDefaults(actual, defaultGVK schema.GroupVersionKind) schema.GroupVersionKind {// actual如果没有设置Kind则用默认的Kind补全if len(actual.Kind) == 0 {actual.Kind = defaultGVK.Kind}// 如果Group和Version都没有设置,则用默认的Group和Version补全。// 为什么必须是都没有设置?缺少Version或者Group有什么问题么?下面的代码给出了答案。if len(actual.Version) == 0 && len(actual.Group) == 0 {actual.Group = defaultGVK.Groupactual.Version = defaultGVK.Version}// 如果Version未设置,则用默认的Version补全,但是前提是Group与默认Group相同。// 因为Group不同的API即便Kind/Version相同可能是两个完全不同的类型,比如自定义资源(CRD)。if len(actual.Version) == 0 && actual.Group == defaultGVK.Group {actual.Version = defaultGVK.Version}// 如果Group未设置而Version与默认的Version相同,为什么不用默认的Group补全?// 前面已经解释过了,应该不用再重复了。return actual
}

Encode

相比于解码,编码就简单很多,直接按照选项(yaml、pretty)编码就行了,源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/serializer/json/json.go#L297

// Encode()实现了Encoder.Encode()接口。
func (s *Serializer) Encode(obj runtime.Object, w io.Writer) error {// CacheableObject允许对象缓存其不同的序列化数据,以避免多次执行相同的序列化,这是一种出于效率考虑设计的类型。// 因为同一个对象可能会多次序列化json、yaml和protobuf,此时就需要根据编码器的标识符找到对应的序列化数据。if co, ok := obj.(runtime.CacheableObject); ok {// CacheableObject笔者不再注释了,感兴趣的读者可以自行阅读源码。// 其实根据传入的参数也能猜出来具体实现:利用标识符查一次map,如果有就输出,没有就调用一次s.doEncode()。return co.CacheEncode(s.Identifier(), s.doEncode, w)}// 非CacheableObject对象,就执行一次json的序列化。return s.doEncode(obj, w)
}// doEncode()类似于于json.Marshal(),只是写入是io.Writer而不是[]byte。
func (s *Serializer) doEncode(obj runtime.Object, w io.Writer) error {// 序列化成yaml?if s.options.Yaml {// 序列化对象为jsonjson, err := caseSensitiveJSONIterator.Marshal(obj)if err != nil {return err}// json->yamldata, err := yaml.JSONToYAML(json)if err != nil {return err}// 写入io.Writer。_, err = w.Write(data)return err}// 输出易于理解的格式?那么问题来了,为什么输出yaml不需要这个判断?很简单,yaml就是易于理解的。if s.options.Pretty {// 序列化对象为jsondata, err := caseSensitiveJSONIterator.MarshalIndent(obj, "", "  ")if err != nil {return err}// 写入io.Writer。_, err = w.Write(data)return err}// 非pretty模式,用我们最常用的json序列化方法,无非我们最常用方法是json.Marshal()。// 如果需要写入io.Writer,下面的代码是标准写法。encoder := json.NewEncoder(w)return encoder.Encode(obj)
}

identifier

前面笔者提到了,编码器标识符可以看做是标签的字符串形式,大家应该很熟悉Kubernetes中的标签选择器,符合标签选择器匹配规则的所有API对象都可以看做"同质"。同样的道理,编码器也有自己的标签,标签相同的所有编码器是同质的,即编码同一个API对象的结果都是一样的。编码器标识符的定义没有那么复杂,就是简单的字符串,匹配也非常简单,标识符相等即为匹配,所以标识符可以理解为标签的字符串形式。源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/serializer/json/json.go#L66

// identifier()根据给定的选项计算编码器的标识符。
func identifier(options SerializerOptions) runtime.Identifier {// 编码器的唯一标识符是一个map[string]string,不同属性的组合形成更了唯一性。result := map[string]string{// 名字是json,表明是json编码器。"name":   "json",// 输出格式为yaml或json"yaml":   strconv.FormatBool(options.Yaml),// 是否为pretty模式"pretty": strconv.FormatBool(options.Pretty),}// 序列化成json,生成最终的标识符,json序列化是标签的一种字符串形式。identifier, err := json.Marshal(result)if err != nil {klog.Fatalf("Failed marshaling identifier for json Serializer: %v", err)}// 也就是说,只要yaml和pretty选项相同的任意两个json.Serializer,任何时候编码同一个API对象输出一定是相同的。// 所以当API对象被多个编码器多次编码时,以编码器标识符为键利用缓冲避免重复编码。return runtime.Identifier(identifier)
}

yaml

其实json.Serializer支持json和ymal,那么还有必要定义yaml.Serializer么?毕竟构造两个json.Serializer对象,一个开启yaml选型,一个关闭yaml选项就可以了。来看看yaml.Serializer的定义,源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/serializer/yaml/yaml.go#L26

// yamlSerializer实现了runtime.Serializer()。
// 没有定义Serializer类型,而是一个包内的私有类型yamlSerializer,如果需要使用这个类型必须通过包内的公有接口创建。
type yamlSerializer struct {// 直接继承了runtime.Serializer,不用想肯定是json.Serializer。runtime.Serializer
}// NewDecodingSerializer()向支持json的Serializer添加yaml解码支持。
// 也就是说yamlSerializer编码json,解码yaml,当然从接口名字看,调用这个接口估计只需要用解码能力吧。
// 好在笔者检索了一下源码,yamlSerializer以及NewDecodingSerializer()没有引用的地方,应该是历史遗留的代码。
func NewDecodingSerializer(jsonSerializer runtime.Serializer) runtime.Serializer {return &yamlSerializer{jsonSerializer}
}// Decode()实现了Decoder.Decode()接口。
func (c yamlSerializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {// yaml->jsonout, err := yaml.ToJSON(data)if err != nil {return nil, nil, err}// 反序列化jsondata = outreturn c.Serializer.Decode(data, gvk, into)
}

总结

  1. json.Serializer可以实现json和yaml两种数据格式的序列化/反序列化,而yaml.Serializer基本不用了;
  2. MetaFactory的功能就是提取apiVersion和kind字段,然后返回GVK;
  3. json.Serializer也可以像json/yaml.Unmarshal()一样使用,只要传入的'into'的类型没有在Schema中注册就可以了;
  4. json.Serializer在反序列化之前需要计算API对象的GVK,计算原则是优先使用json中的GVK,如果GVK有残缺,则采用默认GVK补全,最后用'into'指向的对象类型补全;
  5. 其实runtime.Serializer只是比json/yaml.Unmarshal()多了类型提取并构造对象的过程,但是依然存在无法通用的问题,即解码json和yaml需要不同的对象,这就要RecognizingDecoder来解决了;

Kubernetes的Serializer解析相关推荐

  1. Kubernetes网络技术解析之Pod基于路由模式的通信实现

    前言 Kubernetes集群内,Pod之间可以通信,是Kubernetes网络实现的重要场景之一. Kubernetes通过CNI提供统一的接口和协议,使得我们在使用中可以根据需求自行选择不同的网络 ...

  2. 灵雀云基于 OVN 的 Kubernetes 网络架构解析

    文章目录 Kubernetes 网络的局限性 OVS和OVN网络方案的能力 Kubernetes 网络未来增强的方向 本文为2019-3月26日灵雀云Kubernetes专家刘梦馨在Dockone社区 ...

  3. kubernetes service 原理解析

    原文链接:原文网址 service工作原理: 后续内容详见原文- https://www.jianshu.com/p/879b91d73ea4

  4. Kubernetes API 聚合开发汇总

    2. Kubernetes API 聚合开发 自定义资源实际上是为了扩展 kubernetes 的 API,向 kubenetes API 中增加新类型,可以使用以下三种方式: 修改 kubenete ...

  5. 使用kubeadm安装Kubernetes

    使用kubeadm安装Kubernetes kubeadm是Kubernetes官方提供的用于快速安装Kubernetes集群的工具,伴随Kubernetes每个版本的发布都会同步更新,kubeadm ...

  6. kubernetes 简介:kube-dns 和服务发现

    服务发现 kubernetes 提供了 service 的概念可以通过 VIP 访问 pod 提供的服务,但是在使用的时候还有一个问题:怎么知道某个应用的 VIP?比如我们有两个应用,一个 app,一 ...

  7. kubernetes k8s一文入门

    云原生的定义 云原生技术有利于各组织在公有云.私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用.云原生的代表技术包括容器.服务网格.微服务.不可变基础设施和声明式 API . 这些技术能够构 ...

  8. 一文了解 Kubernetes 中的服务发现

    原文链接:一文了解 Kubernetes 中的服务发现 Kubernetes 服务发现是一个经常让我产生困惑的主题之一.本文分为两个部分: 网络方面的背景知识 深入了解 Kubernetes 服务发现 ...

  9. 在CentOS 7上安装使用Kubernetes:管理云平台多个主机上的容器化应用

    Table of Contents 安装Kubernetes Master和Minions 验证网络 ServiceAccount错误 玩K8S 运行Kubernetes留言簿(无GCE,无DNS) ...

  10. kubeadm方式安装kubernetes

    一.kubenetes搭建方式有三种:         1.minikube (通常在测试环境使用,不要在生产环境中使用)         2.kubeadm (是一种快速部署kubernetes的方 ...

最新文章

  1. 谈谈我对Promise的理解
  2. 静态网页与动态网页区别
  3. 在JSP页面中使用Ajax主题时的引入
  4. JUC 中断线程的3种方式
  5. Vue入门 ---- 简易留言板
  6. nginx1.8.1反向代理、负载均衡功能的实现
  7. ZKWeb网页框架2.1正式发布
  8. 一拳超人最强之男手游辅助脚本 快速升级提高提高战力
  9. 金蝶k3服务器重装账套怎么恢复,金蝶KIS旗舰版、金蝶K3账套备份、恢复操作教程...
  10. css中“~”(波浪号)、“,”(逗号)、 “ + ”(加号)和 “ ”(大于号)是什么意思?
  11. 瑞幸咖啡2022,摆脱困境,迎来坦途
  12. 如何用photoshop切图并去掉图片上面的文字
  13. 新手操作更换固态和重装系统竟然这么麻烦?!
  14. java aes 中文_从Java到Python的AES加密
  15. 沉密神采,如对至尊 --中国书法
  16. linux--shell--crontab定义运行任务
  17. DJI大疆 windows SDK开发入门(1) integrate SDK into Application
  18. Java 编译器代码定义的 Java语言的类型 Types
  19. [Shader]制作一个电流扩散效果
  20. 在程序运行时,我们遇到的200,404,500分别代表什么!

热门文章

  1. 单纯形法的代码实现与退化算例
  2. DPCM差分预测编码原理及实现
  3. 宝峰c1对讲机写频软件_宝峰888s写频软件
  4. txt文本保存操作(新建文件夹以及保存txt文本)
  5. sendip linux发包工具
  6. 计算机原理与结构 实验1《实验工具使用》
  7. DirectX9学习(四)装载位图
  8. 体验经济:互联网生存的秘密
  9. GB50174《电子信息系统机房设计规范》贯标培训通知
  10. (五)2005年我的第一次软件行业创业,烧掉30万、2年时间打水漂的惨痛教训