云原生之深入解析Kubernetes Operator的最佳实践和最常见的问题分析

一、Kubernetes Operator 简介

  • Kubernetes Operator 是通过连接主 API 并 watch 时间的一组进程,一般会 watch 有限的资源类型。当相关 watch 的 event 触发的时候,operator 做出响应并执行具体的动作。这可能仅限于与主 API 交互,但通常会涉及在其他一些系统上执行某些操作(可以是集群中或集群外资源)。
  • Kubernetes 操作员是连接到主 API 并监视事件的进程,通常是在有限数量的资源类型上。当相关事件发生时,操作员做出反应并执行特定的操作。

在这里插入图片描述

  • 操作符被实现为一组控制器,其中每个控制器监视一个特定的资源类型。当被监视资源上发生相关事件时,将启动一个协调周期。Operators 是控制器的集合,并且每个控制器 watch 指定的资源类型。当被 watched 的资源时间触发的时候,协调周期也将随之启动。在协调周期期间,控制器有责任检查当前状态是否与被 watched 资源描述的期望状态相匹配。有趣的是,根据设计,时间并不会传递到协调周期中,这将会强制地让你去考虑实例的整个状态,这种方法被称为基于水平触发而不是基于边缘触发(level-based, as opposed to edge-based)。这源自于电子电路的设计,水平触发是接收 event(例如中断)并对状态做出反应的理念,而基于边缘的触发是接收 event 并对状态变化做出反应的理念。
  • 水平触发虽说效率较低,因为它强制重新评估完整的状态,而不是仅仅关注改变了什么,但在信号可能丢失或多次重复传输的复杂不可靠环境中,这种方式被认为是更适用的。这种设计的选择会影响我们编写控制器代码的方式。如下提供了一个高层次的总结:

在这里插入图片描述

  • 当向API服务器发送请求时,特别是对于创建和删除请求,它们将会经历上述的阶段。需要注意的是,也可以指定 webhook 来执行请求的更改和验证。如果 operator 引入 CRD(custom resource definition),可能还必须定义这些 webhook。一般来说,operator 进程会开放一个端口来来实现 webhook endpoint。
  • 如果 operator 引入了一个新的 CRD,Operator SDK 将会协助你来搭建,为确保 CRD 符合 Kubernetes 扩展 API 的最佳实践,请遵循这些约定。这里所提到的所有的最佳实践都在 operator-utils 代码库中,并以可运行的例子体现。在 operator 项目中,也可以将 operator-utils 以 library 的方式导入,以此提供一些有用的工具。

二、创建 watches

  • 正如我们所说,控制器监视资源上的事件,这是通过对手表的抽象来实现的。watch 是一种接收某种类型(核心类型或 CRD)的机制。一般通过指定以下内容来创建 watch 机制:
    • 想要 watch 的资源类型;
    • handler 将被监视类型上的 events 映射到一个或多个调用协调周期的实例,监视类型和实例类型不必相同;
    • predicate 是一组能够过滤 events 且可自定义的函数。
  • 如下记录了以上提及的内容:

在这里插入图片描述

  • 通常来说,同一类型(kind)开启多个 watch 是可行的,因为 watch 是多路复用的。也应该尽可能多地尝试过滤 event,这边有个 predicate 例子,用来过滤 secret 资源上的 event,这里只对类型为 TLS 的 secret 资源 event 感兴趣:
isAnnotatedSecret := predicate.Funcs{
    UpdateFunc: func(e event.UpdateEvent) bool {
        oldSecret, ok := e.ObjectOld.(*corev1.Secret)
        if !ok {
            return false
        }
        newSecret, ok := e.ObjectNew.(*corev1.Secret)
        if !ok {
            return false
        }
        if newSecret.Type != util.TLSSecret {
            return false
        }
        oldValue, _ := e.MetaOld.GetAnnotations()[certInfoAnnotation]
        newValue, _ := e.MetaNew.GetAnnotations()[certInfoAnnotation]
        old := oldValue == "true"
        new := newValue == "true"
        // if the content has changed we trigger if the annotation is there
        if !reflect.DeepEqual(newSecret.Data[util.Cert], oldSecret.Data[util.Cert]) ||
            !reflect.DeepEqual(newSecret.Data[util.CA], oldSecret.Data[util.CA]) {
            return new
        }
        // otherwise we trigger if the annotation has changed
        return old != new
    },
    CreateFunc: func(e event.CreateEvent) bool {
        secret, ok := e.Object.(*corev1.Secret)
        if !ok {
            return false
        }
        if secret.Type != util.TLSSecret {
            return false
        }
        value, _ := e.Meta.GetAnnotations()[certInfoAnnotation]
        return value == "true"
    },
}
  • 一个非常常见的模式是观察创建(和拥有)资源上的 events,并且定期在拥有这些资源的 CR 上执行协调周期 reconcile cycle。为此,可以使用 EnqueueRequestForOwner handler,按照如下方式完成:
err = c.Watch(&source.Kind{Type: &examplev1alpha1.MyControlledType{}}, &handler.EnqueueRequestForOwner{})
  • 另一种不太常用的情况是将一个 events 传播到多个资源上。考虑一种情况,一个控制器注入了 TLS secret 的路由,同一个命名空间中的多个路由可以指向同一个 secret。如果 secret 发生了改变,需要更新所有路由。因此,需要在 secret 类型上创建一种 watch 机制,处理程序如下所示:
type enqueueRequestForReferecingRoutes struct {
        client.Client
}

// trigger a router reconcile event for those routes that reference this secret
func (e *enqueueRequestForReferecingRoutes) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
        routes, _ := matchSecret(e.Client, types.NamespacedName{
                Name:      evt.Meta.GetName(),
                Namespace: evt.Meta.GetNamespace(),
        })
        for _, route := range routes {
                q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
                        Namespace: route.GetNamespace(),
                        Name:      route.GetName(),
                }})
        }
}

// Update implements EventHandler
// trigger a router reconcile event for those routes that reference this secret
func (e *enqueueRequestForReferecingRoutes) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
        routes, _ := matchSecret(e.Client, types.NamespacedName{
                Name:      evt.MetaNew.GetName(),
                Namespace: evt.MetaNew.GetNamespace(),
        })
        for _, route := range routes {
                q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
                        Namespace: route.GetNamespace(),
                        Name:      route.GetName(),
                }})
        }
}

三、资源 Reconciliation Cycle

  • 协调周期 reconcile cycle 是在被 watch 的 event 传递后框架将控制权转交给我们地方。正如之前所解释的,在该 reconcile cycle 中没有获得相关时间类型的信息,是因为是基于水平触发的方式来工作。
  • 如下是一个管理 CRD 控制器的常见 reconcile cycle 的模型,和其他任何一个模型一样,它不会反映任何特定用例,但希望它将有助于解决编写 operator 时遇到的问题:

在这里插入图片描述

  • 从上图中可以看到,主要步骤是:
    • 检索感兴趣的 CR 实例;
    • 确认实例的有效性,不会在不合法实例上做任何事情;
    • 初始化实例:如果实例的某些值没有被初始化,会在这一步进行处理;
    • 判断实例的 deletion 状态,如果实例正在被删除,也需要做一些特殊的清理。
  • 管理控制器的业务逻辑,如果以上步骤均通过,最终可以管理和执行该实例的 reconcile 逻辑,这个逻辑每个控制器都不尽相同。

四、资源验证

  • 这里存在两种类型的校验:
    • 语法校验:通过定义 OpenAPI 规则来验证;
    • 语义校验:可以通过创建 ValidatingAdmissionConfiguration 来完成。
  • 注意:在控制器中不能校验 CR 合法性,一旦 CR 被 API Server 接受了,它就会存在 Etcd 中,CR 存在 Etcd 之后,管理该 CR 资源的控制器就无法拒绝它,如果这个 CR 是不合法的,控制器在尝试使用或处理它的时候将会发生错误。
  • 推荐:由于不能保证 ValidatingAdmissionConfiguration 被创建或正常工作,还是应该在控制器内部去验证 CR,如果 CR 不合法,应该避免创建无限错误循环。

① 语法校验

  • 可以按照Generating CRD的描述添加 OpenAPI 验证规则。
  • 推荐:尽可能多地为自定义资源模型进行语法校验,尽量使用语法校验,因为它相对简单,并且可以防止格式错误的 CR 存储在 etcd 中。

② 语义校验

  • 语义校验是为了确保字段具有合理的值,从而使整个资源记录是有意义的。语义验证业务逻辑取决于 CR 所代表的概念,并且必须由 operator 的开发人员进行编码实现。
  • 如果给定的 CR 需要语义校验,那么 operator 需要暴露一个 webhook,作为 operator deploymen 的一部分,ValidatingAdmissionConfiguration 也应该被创建。
  • 目前存在的局限性:
    • 在 OpenShift 3.11 中,ValidatingAdmissionConfigurations 还处于技术预览阶段(将从 4.1 开始支持);
    • Operator SDK 不支持脚手架形式的 webhook,可以使用 kubebuilder 来进行实现:
kubebuilder webhook --group crew --version v1 --kind FirstMate --type=mutating --operations=create,update

③ 验证控制器中的资源

  • 最好的方式是直接拒绝一个无效的 CR,而不是接受并保存在 Etcd 中,然后对它进行错误条件处理。当然也有可能的情况是,ValidatingAdmissionConfiguration 并没有被部署或者根本不可用,因此在控制器代码中进行语义校验仍然是一个很好的做法,应该做到的是,可以在 ValidatingAdmissionConfiguration 和控制器之间共享这部分结构化的代码。
  • 控制器中调用验证方法的代码如下所示:
if ok, err := r.IsValid(instance); !ok {
    return r.ManageError(instance, err)
}
  • 请注意,如果验证失败,按照错误管理部分中的描述来管理这个错误。IsValid 函数如下:
func (r *ReconcileMyCRD) IsValid(obj metav1.Object) (bool, error) {
    mycrd, ok := obj.(*examplev1alpha1.MyCRD)
 // validation logic
}

五、资源初始化

  • Kubernetes 的一个很好的惯例是用户只初始化他所需要的资源字段,其他的可以省略。但从编码人员和调试者的角度来说,实际上最好将所有的字段都初始化,这允许在编码的时候不必总是去校验字段是否被定义了,并且可以轻松地排除错误情况。
  • 为了初始化资源,这里有两个选项:
    • 在控制器中定义初始化方法;
    • 定义一个 MutatingAdmissionConfiguration(类似于 ValidatingAdmissionConfiguration 的程序);
  • 在控制器中定义一个初始化方法,代码应类似于此示例:
if ok := r.IsInitialized(instance); !ok {
    err := r.GetClient().Update(context.TODO(), instance)
    if err != nil {
        log.Error(err, "unable to update instance", "instance", instance)
        return r.ManageError(instance, err)
    }
    return reconcile.Result{}, nil
}
  • 如果 IsInitialized 方法的结果返回 true,更新 instance 并 return,这将会立即出发另一个 reconcile cycle,第二次调用 IsInitialized 方法将会返回 false,代码逻辑将会执行到下一部分。

① 资源 Finalization

  • 如果资源不属于操作员控制的 CR,但在删除该 CR 时需要采取措施,必须使用 finalizer。终结器提供了一种机制来通知 Kubernetes 控制平面,在执行标准 Kubernetes 垃圾收集逻辑之前需要执行一个操作。资源可以有一个或多个 finalizers,每一个控制器应该管理自己的 finalizer 并且忽略其他的。
  • 管理 finalizers 的伪代码算法:
    • 如果需要,在初始化方法中添加 finalizer。
    • 当资源被删除,检查此控制器拥有的 finalizer 是否存在。
      • 清理成功,移除 finalizer 并更新 CR;
      • 如果失败决定是重试还是放弃并可能留下垃圾(在某些情况下这是可以接受的);
      • 如果不存在,直接 return;
      • 如果存在,执行如下清理逻辑:如果清理逻辑需要添加额外的资源,需要记住的是,无法在正在删除的命名空间中创建其他资源,删除命名空间将会触发 finalizer 并删除其下所有资源。
  • 代码如下所示:
if util.IsBeingDeleted(instance) {
    if !util.HasFinalizer(instance, controllerName) {
        return reconcile.Result{}, nil
    }
    err := r.manageCleanUpLogic(instance)
    if err != nil {
        log.Error(err, "unable to delete instance", "instance", instance)
        return r.ManageError(instance, err)
    }
    util.RemoveFinalizer(instance, controllerName)
    err = r.GetClient().Update(context.TODO(), instance)
    if err != nil {
        log.Error(err, "unable to update instance", "instance", instance)
        return r.ManageError(instance, err)
    }
    return reconcile.Result{}, nil
}

② 资源所有权

  • 资源所有权是 Kubernetes 中的原生概念,它决定了资源如何被删除。默认情况下,当一个资源被删除的时候,它的子资源也也会被删除(可以设置 cascade=false 来关闭这种行为)。这种行为有助于确保资源的正确垃圾收集,尤其是当资源控制多级层次结构中的其他资源时(deployment-> repilcaset->pod)。
  • 建议:如果控制器创建资源并且它的生命周期与其他资源(kubernetes 核心资源或其他 CR)有关联,那么应该将此资源设置为其他资源的所有者,如下所示:
controllerutil.SetControllerReference(owner, obj, r.GetScheme())
  • 有关所有权的其他规则如下:
    • 父子资源必须位于同一命名空间中;
    • 命名空间资源可以拥有集群资源,一个对象可以有一个所有者列表,如果多个命名空间对象拥有相同的集群资源,则每个对象都应声明所有权,而不会覆盖其他对象的所有权;
    • 集群资源不能拥有命名空间资源;
    • 集群资源可以拥有另外一个集群资源。

六、状态管理

  • Status 是资源的一个标准部分,被用于报告资源的状态。在这里将使用 status 报告最后一次执行协调循环的结果,也可以在 Status 中添加更多的信息。
  • 在正常情况下,如果每次执行 reconcile cycle 的时候都要更新资源,这将触发更新时间,进而导致无限触发 reconcile cycle。因此,正如上面描述的那样,应该把 Status 作为子资源。使用这种方法,能够不增加 ResourceGeneration 元数据域的情况下更新资源的状态。
  • 使用如下命令更新状态:
err = r.Status().Update(context.Background(), instance)
  • 现在需要为 watch 机制写一个 predicate,用来丢弃不增加 ResourceGeneration 的更新事件,可以使用 GenerationChangePredicate 来完成此功能。上文提到过,在使用 finalizer 的时候,应该在初始化的时候设置,如果 finalizer 是初始化的唯一项,由于它是元数据项的一部分,因此 ResourceGeneration 不会递增。
  • 为了说明该用例,以下是 predicate 的修改版本:
type resourceGenerationOrFinalizerChangedPredicate struct {
 predicate.Funcs
}

// Update implements default UpdateEvent filter for validating resource version change
func (resourceGenerationOrFinalizerChangedPredicate) Update(e event.UpdateEvent) bool {
 if e.MetaNew.GetGeneration() == e.MetaOld.GetGeneration() && reflect.DeepEqual(e.MetaNew.GetFinalizers(), e.MetaOld.GetFinalizers()) {
  return false
 }
 return true
}
  • 假设 status 如下所示:
type MyCRStatus struct {
 // +kubebuilder:validation:Enum=Success,Failure
 Status     string      `json:"status,omitempty"`
 LastUpdate metav1.Time `json:"lastUpdate,omitempty"`
 Reason     string      `json:"reason,omitempty"`
}
  • 可以写一个函数来管理并保证 reconcile cycle 成功执行:
func (r *ReconcilerBase) ManageSuccess(obj metav1.Object) (reconcile.Result, error) {
 runtimeObj, ok := (obj).(runtime.Object)
 if !ok {
  log.Error(errors.New("not a runtime.Object"), "passed object was not a runtime.Object", "object", obj)
  return reconcile.Result{}, nil
 }
 if reconcileStatusAware, updateStatus := (obj).(apis.ReconcileStatusAware); updateStatus {
  status := apis.ReconcileStatus{
   LastUpdate: metav1.Now(),
   Reason:     "",
   Status:     "Success",
  }
  reconcileStatusAware.SetReconcileStatus(status)
  err := r.GetClient().Status().Update(context.Background(), runtimeObj)
  if err != nil {
   log.Error(err, "unable to update status")
   return reconcile.Result{
    RequeueAfter: time.Second,
    Requeue:      true,
   }, nil
  }
 } else {
  log.Info("object is not RecocileStatusAware, not setting status")
 }
 return reconcile.Result{}, nil
}

七、错误管理

  • 如果控制器进入了一个错误条件,并且在 reconcile 方法中返回了一个错误,operator 将会打印错误日志到标准输出,reconlie event 将会立即再次调度(默认的调度器实际上应该检测是否一遍又一遍地出现相同的错误,并增加相应的调度时间,以经验看来,这并没有发生)。如果错误一直存在,那么也将永远存在错误循环,而且这个错误条件对用户来说是不可见的。
  • 有两种方法可以通知用户发生了错误,它们可以同时使用:
    • 在对象的 status 字段中返回错误;
    • 生成一个 event 描述错误。
  • 此外,如果错误能够自解决,应该在一段周期时间后重新调度 reconcile cycle。通常来说,周期时间是呈指数增长的,因此在每次迭代中,reconcile event 周期会越来越长(例如每次增长时间量的两倍)。
  • 现在构建状态管理来处理错误条件:
func (r *ReconcilerBase) ManageError(obj metav1.Object, issue error) (reconcile.Result, error) {
    runtimeObj, ok := (obj).(runtime.Object)
    if !ok {
        log.Error(errors.New("not a runtime.Object"), "passed object was not a runtime.Object", "object", obj)
        return reconcile.Result{}, nil
    }
    
    var retryInterval time.Duration
    r.GetRecorder().Event(runtimeObj, "Warning", "ProcessingError", issue.Error())
    if reconcileStatusAware, updateStatus := (obj).(apis.ReconcileStatusAware); updateStatus {
        lastUpdate := reconcileStatusAware.GetReconcileStatus().LastUpdate.Time
        lastStatus := reconcileStatusAware.GetReconcileStatus().Status
        status := apis.ReconcileStatus{
            LastUpdate: metav1.Now(),
            Reason:     issue.Error(),
            Status:     "Failure",
        }

        reconcileStatusAware.SetReconcileStatus(status)
        err := r.GetClient().Status().Update(context.Background(), runtimeObj)
        if err != nil {
            log.Error(err, "unable to update status")
            return reconcile.Result{
                RequeueAfter: time.Second,
                Requeue:      true,
            }, nil
        }

        if lastUpdate.IsZero() || lastStatus == "Success" {
            retryInterval = time.Second
        } else {
            retryInterval = status.LastUpdate.Sub(lastUpdate).Round(time.Second)
        }
    } else {
        log.Info("object is not RecocileStatusAware, not setting status")
        retryInterval = time.Second
    }

    return reconcile.Result{
        RequeueAfter: time.Duration(math.Min(float64(retryInterval.Nanoseconds()*2), float64(time.Hour.Nanoseconds()*6))),
        Requeue:      true,
    }, nil
}
  • 注意,此函数会立即发送一个 event,然后使用错误条件更新状态,最后计算何时重新安排下一次 reconcile,该算法尝试将每个循环的时间加倍,最多到六个小时为止。六个小时是一个很好的上限时间,因为 event 大约持续 6 个小时,所以这应该确保始终有一个活动 event 描述当前的错误情况。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/250869.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python快速实现简单的图片透明化

整张图片透明化的完整代码如下: import os import glob from PIL import Imagedef convert_to_transparent(image_path, output_folder):image Image.open(image_path)image image.convert("RGBA")data image.getdata()new_data []for item in data:…

verilog语法进阶-分布式ram原语

概述 官方提供的原语 RAM16X1S_1 #(.INIT(16h0000) // Initial contents of RAM) RAM16X1S_1_inst (.O(O), // RAM output.A0(A0), // RAM address[0] input.A1(A1), // RAM address[1] input.A2(A2), // RAM address[2] input.A3(A3), // RAM address[3…

SpringMVC上传下载文件解读

知识点 文件上传(File Upload): 创建一个控制器方法,使用 MultipartFile 参数来接收上传的文件。在 Spring 配置文件中配置一个 MultipartResolver,常用的实现类是 CommonsMultipartResolver。在 MultipartResolver …

PyTorch官网demo解读——第一个神经网络(1)

神经网络如此神奇,feel the magic 今天分享一下学习PyTorch官网demo的心得,原来实现一个神经网络可以如此简单/简洁/高效,同时也感慨PyTorch如此强大。 这个demo的目的是训练一个识别手写数字的模型! 先上源码: fr…

crmeb v5新增一个功能的完整示例记录

首先,需求 工作中的二开需求是这样的,修改首页的装修,并新增回收报价的功能 开始动手 第一步,我们要到后面的管理界面,去装修中修改首面的展示 首页的页面配置好之后,就要在 前端的展示程序中 配置相…

105基于matlab的阶次分析算法

基于matlab的阶次分析算法,用于变转速机械故障特征提取,可运行,包含寻找脉冲时刻,等角度时刻。数据可更换自己的,程序已调通,可直接运行。 105阶次分析变转速信号处理 (xiaohongshu.com)

二十七、读写文件

二十七、读写文件 27.1 文件类QFile #include <QCoreApplication>#include<QFile> #include<QDebug>int main(int argc, char *argv[]) {QCoreApplication a(argc, argv);QFile file("D:/main.txt");if(!file.open(QIODevice::WriteOnly | QIODe…

Arrays.asList()方法:陷阱与解决之道

在Java编程中&#xff0c;Arrays类提供了一系列用于操作数组的实用方法。其中&#xff0c;​Arrays.asList()​方法是一个常用的方法&#xff0c;用于快速将数组转换为List集合。然而&#xff0c;这个方法存在一些潜在的陷阱&#xff0c;可能导致出现意外的行为。本文将介绍​A…

数据可视化---柱状图

import matplotlib.pyplot as plt import numpy as npdef plot_bar_chart(data, labels, colorsNone, title"Bar Chart", xlabel"X-Axis", ylabel"Y-Axis"):"""绘制柱状图&#xff0c;并在柱子上显示数量和比例。:param data: 包…

【C++11特性篇】C++11中新增的initializer_list——初始化的小利器(2)

前言 大家好吖&#xff0c;欢迎来到 YY 滴C11系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; 目录 一.探究std::initializer_list是什么…

OceanBase 4.2.1社区版 最小资源需求安装方式

OceanBase 4.2.1社区版 最小资源需求安装方式 资源需求 资源需求分析 observer Memory 控制参数&#xff1a; memory_limit_percentage 默认80% memory_limit 直接设定observer Memory 大小 System memory 可设为1G 租户内存:sys租户内存设为1G&#xff0c;OCP需要的租户oc…

Mac brew install youtube-dl 【 youtube 下载工具:youtube-dl 安装】

文章目录 1. 简介2. 预备3. 安装4. 命令5. 测试 1. 简介 youtube-dl - 从youtube.com或其他视频平台下载视频 https://github.com/ytdl-org/youtube-dl 2. 预备 安装并配置 git安装 brew 3. 安装 MacBook-Pro ~ % brew install youtube-dl Warning: youtube-dl has been …

【Java系列】详解多线程(三)—— 线程安全(上篇)

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【Java系列专栏】【JaveEE学习专栏】 本专栏旨在分享学习Java的一点学习心得&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 一…

基于EasyExcel的数据导入导出

前言&#xff1a; 代码复制粘贴即可用&#xff0c;主要包含的功能有Excel模板下载、基于Excel数据导入、Excel数据导出。 根据实际情况修改一些细节即可&#xff0c;最后有结果展示&#xff0c;可以先看下结果&#xff0c;是否是您想要的。 台上一分钟&#xff0c;台下60秒&a…

OSG中几何体的绘制(二)

5. 几何体操作 在本章的前言中就讲到&#xff0c;场景都是由基本的绘图基元构成的&#xff0c;基本的绘图基元构成简单的几何体,简单的几何体构成复杂的几何体&#xff0c;复杂的几何体最终构造成复杂的场景。当多个几何体组合时&#xff0c;可能存在多种降低场景渲染效率的原因…

SQL进阶理论篇(七):B+树的查询及存储机制

文章目录 简介数据库中的存储结构数据库中的页结构从数据页来看B树的查询过程总结参考文献 简介 我们之前已经了解过数据库的B树索引和Hash索引&#xff0c;这些索引信息以及数据记录都是保存在文件里的&#xff0c;确切的说是存储在页结构中。 本节&#xff0c;从我们将了解…

【LeetCode刷题】-- 161.相隔为1的编辑距离

161.相隔为1的编辑距离 方法&#xff1a;一次遍历 首先&#xff0c;我们要确认字符串的长度不会相差太远。如果长度差了2个或更多字符&#xff0c;那么 s 和 t 就不可能是一次编辑之差的字符串。 接下来&#xff0c;我们假设 s 的长度总是短于或等于 t 的长度。如果不是这样&…

cesium 自定义贴图,shadertoy移植教程。

1.前言 cesium中提供了一些高级的api&#xff0c;可以自己写一些shader来制作炫酷的效果。 ShaderToy 是一个可以在线编写、测试和分享图形渲染着色器的网站。它提供了一个图形化的编辑器&#xff0c;可以让用户编写基于 WebGL 的 GLSL 着色器代码&#xff0c;并实时预览渲染结…

大数据存储技术(3)—— HBase分布式数据库

目录 一、HBase简介 &#xff08;一&#xff09;概念 &#xff08;二&#xff09;特点 &#xff08;三&#xff09;HBase架构 二、HBase原理 &#xff08;一&#xff09;读流程 &#xff08;二&#xff09;写流程 &#xff08;三&#xff09;数据 flush 过程 &#xf…

配置802.1x认证

实验目的&#xff1a; 某公司拥有两个部门&#xff0c;市场部和人事部门&#xff0c;市场部和人事部的IP地址分别为10.1.11.0/24、10.1.21.0/24两个IP网段。市场部属于vlan11&#xff0c;人事部属于vlan21。现在需要在SW2上配置802.1x认证&#xff0c;实现终端用于只有认证成功…