点击上方“程序猿技术大咖”,关注并选择“设为星标”

回复“加群”获取入群讨论资格!

本篇文章来自《华为云云原生王者之路训练营》钻石系列课程第2课,由华为云容器服务架构师Leo主讲,详细讲解了kubernetes核心机制的实现原理和设计精髓,包括List-Watch机制和Informer模块,以及kubernetes  controller机制原理。

1

Kubernetes系统架构详解

Kubernetes总体架构

Kubernetes是Google开源的容器集群管理系统,它构建在容器技术之上,为容器化的应用提供资源调度,部署运行,服务发现,扩容缩容等一整套功能,本质上是基于容器技术的Micro-PaaS平台,Kubernetes的灵感来源于Google内部的Borg系统。

主要目的是将容器宿主机组成集群,统一进行资源调度,自动管理容器生命周期,提供跨节点服务发现和负载均衡;更好的支持微服务理念,划分、细分服务之间的边界,比如lablel、pod等概念的引入。目前主要的发展方向是可插件化和可扩展性进行引进,框架越来越轻量化,插件可定制化的东西也越来越多等。

Kubernetes主要包括管控面和数据面,管控面主要涉及用户接触很少的用于管理K8s资源的核心组件,数据面主要是实际运行用户的业务。涉及的核心组件有API server、controller、kubelet等。

Kubernetes 核心组件

1)控制面上的组件:

Etcd:etcd 是兼具一致性和高可用性的键值数据库,可以作为保存 Kubernetes 所有集群数据的后台数据库。etcd支持watch,这样组件很容易得到系统状态的变化,从而快速响应和协调工作

kube-apiserver:主要提供Kubernetes API,提供对Pods,Services,ReplicationController等对象的CRUD处理REST操作,验证它们,在etcd中更新相应的对象API不仅仅是面向最终用户的,同时也是面向工具和扩展开发者的,是开放生态系统的基础

kube-scheduler:通过访问Kubernetes中/binding API, Scheduler负责Pods在各个节点上的分配,Scheduler是插件式的,Kubernetes将来可以支持用户自定义的scheduler

kube-controller-manager:控制器循环监听集群中资源状态,按照预期状态对资源进行管理。每个控制器就是将对应的资源牵引到期望的状态,Kubernetes将来可以把这些控制器拆分并提供可插拔的组件

cloud-controller-manager:云控制器管理器是指嵌入特定云的控制逻辑的 控制平面组件。云控制器管理器允许您链接聚合到云提供商的应用编程接口中, 并分离出相互作用的组件与您的集群交互的组件。

2)数据面节点上的组件:

Kubelet:Kubelet管理pods和它们的容器、镜像、卷等

Kube-proxy:Kube-proxy是一个简单的网络代理和负载均衡器,它具体实现Service模型,每个Service都会在所有的Kube-Proxy节点上体现,根据Service的selector所覆盖的Pods, 对这些Pods做负载均衡来服务于Service的访问者

容器运行时(Container Runtime):容器运行环境是负责运行容器的软件。Kubernetes 支持多个容器运行环境: Docker、 containerd、CRI-O 以及任何实现 Kubernetes CRI (容器运行环境接口)。

3)插件(Addons):

DNS:集群 DNS 是一个 DNS 服务器,和环境中的其他 DNS 服务器一起工作,它为 Kubernetes 服务提供 DNS 记录。Kubernetes 启动的容器自动将此 DNS 服务器包含在其 DNS 搜索列表中。

2

controller控制器原理详解

Kubernetes Controller Manager原理解析

Controller Manager 是集群内部的管理控制中心,负责统一管理与运行不同的 Controller ,实现对集群内的 Node、Pod 等所有资源的管理。比如当通过 Deployment 创建的某个 Pod 发生异常退出时,RS Controller 便会接受并处理该退出事件,并创建新的 Pod 来维持预期副本数。

controller manager的作用:

k8s内部几乎每种特定资源都有特定的 Controller 维护管理,而 Controller Manager 的职责便是把所有的 Controller 聚合起来:

  • 提供基础设施降低 Controller 的实现复杂度

  • 启动和维持 Controller 的正常运行,watch api-server,然后对不同的 Controller 分发事件通知。

K8s中有几十种 Controller,这里列举一些相对重要的Controller:

  • 部署控制器(Deployment Controller):负责pod的滚动更新、回滚以及支持副本的水平扩容等。

  • 节点控制器(Node Controller): 负责在节点出现故障时进行通知和响应。

  • 副本控制器(Replication Controller): 负责为系统中的每个副本控制器对象维护正确数量的 Pod。

  • 端点控制器(Endpoints Controller): 填充端点(Endpoints)对象(即加入 Service 与 Pod)。

  • 服务帐户和令牌控制器(Service Account & Token Controllers): 为新的命名空间创建默认帐户和 API 访问令牌

Controller 工作流程

Controller Manager 主要提供了一个分发事件的能力,而不同的 Controller 只需要注册对应的 Handler 来等待接收和处理事件。

在 Controller Manager 的帮助下,Controller 的逻辑可以做的非常纯粹,只需要实现相应的 EventHandler 即可。以Deployment controller为例:

List & Watch:

  • Controller manager与api-server的通信主要通过两种方式:List 和 Watch。

  • List是短连接实现,用于获取该资源的所有object;

  • Watch是长连接实现,用于监听在List中获取的资源的变换。

  • api-server检测到资源产生变更时,会主动通知到Controller manager(利用分块传输编码)。

client-go:

  • client-go实现统一管理每种 Controller 的List和Watch。

  • 将收到的event事件放到缓存中,异步分发给每个 Controller 的注册的eventHandler。

Controller中的eventHandler如何注册?

在pkg/controller/deployment/deployment_controller.go 的 NewDeploymentController 方法中,便包括了 Event Handler 的注册,对于 Deployment Controller 来说,只需要根据不同的事件实现不同的处理逻辑,便可以实现对相应资源的管理。

AddEventHandler被封装成ProcessListener并添加到数组中,并且调用了ProcessListener的run方法。

// NewDeploymentController creates a new DeploymentController.
func NewDeploymentController(dInformer appsinformers.DeploymentInformer, rsInformer appsinformers.ReplicaSetInformer, podInformer coreinformers.PodInformer, client clientset.Interface) (*DeploymentController, error) {
... ...dInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc:    dc.addDeployment,UpdateFunc: dc.updateDeployment,// This will enter the sync loop and no-op, because the deployment has been deleted from the store.DeleteFunc: dc.deleteDeployment,})rsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc:    dc.addReplicaSet,UpdateFunc: dc.updateReplicaSet,DeleteFunc: dc.deleteReplicaSet,})podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{DeleteFunc: dc.deletePod,})dc.syncHandler = dc.syncDeploymentdc.enqueueDeployment = dc.enqueuedc.dLister = dInformer.Lister()dc.rsLister = rsInformer.Lister()dc.podLister = podInformer.Lister()dc.dListerSynced = dInformer.Informer().HasSynceddc.rsListerSynced = rsInformer.Informer().HasSynceddc.podListerSynced = podInformer.Informer().HasSyncedreturn dc, nil
}

client-go under the hood

kubernetes 在 github 上提供了一张 client-go 的架构图,从中可以看出,Controller 正是下半部分(CustomController)描述的内容,而client-go主要完成的是上半部分。

client-go组件:

  • Reflector:reflector用来watch特定的k8s API资源。具体的实现是通过ListAndWatch的方法,watch可以是k8s内建的资源或者是自定义的资源。当reflector通过watch API接收到有关新资源实例存在的通知时,它使用相应的列表API获取新创建的对象,并将其放入watchHandler函数内的Delta Fifo队列中。

  • Informer:informer从Delta Fifo队列中弹出对象。执行此操作的功能是processLoop。base controller的作用是保存对象以供以后检索,并调用我们的控制器将对象传递给它。

  • Indexer:索引器提供对象的索引功能。典型的索引用例是基于对象标签创建索引。Indexer可以根据多个索引函数维护索引。Indexer使用线程安全的数据存储来存储对象及其键。在Store中定义了一个名为MetaNamespaceKeyFunc的默认函数,该函数生成对象的键作为该对象的 / 组合。

自定义controller组件:

  • Informer reference:指的是Informer实例的引用,定义如何使用自定义资源对象。自定义控制器代码需要创建对应的Informer。

  • Indexer reference: 自定义控制器对Indexer实例的引用。自定义控制器需要创建对应的Indexer。

client-go中提供NewIndexerInformer函数可以创建Informer 和 Indexer。

  • Resource Event Handlers:资源事件回调函数,当它想要将对象传递给控制器时,它将被调用。编写这些函数的典型模式是获取调度对象的key,并将该key排入工作队列以进行进一步处理。

  • Workqueue:任务队列。编写资源事件处理程序函数以提取传递的对象的key并将其添加到任务队列。

  • Process Item:处理任务队列中对象的函数, 这些函数通常使用Indexer引用或Listing包装器来重试与该key对应的对象。

3

list-watch机制原理详解

Informer封装list-watch

K8s的informer模块封装list-watch API,用户只需要指定资源,编写事件处理函数,AddFunc,UpdateFunc和DeleteFunc等。

Informer是Client-go中的一个核心工具包。为了让Client-go更快地返回List/Get请求的结果、减少对Kubenetes API的直接调用,Informer被设计实现为一个依赖Kubernetes List/Watch API、可监听事件并触发回调函数的二级缓存工具包。

Informer设计实现

Informer组件:

  • Controller 用于处理收到的事情,触发Processor中的回调函数

  • Reflector:通过Kubernetes Watch API监听resource下的所有事件

  • Lister:用来被调用List/Get方法

  • Processor:记录并触发回调函数

  • DeltaFIFO和LocalStore:DeltaFIFO和LocalStore是Informer的两级缓存。DeltaFIFO用来存储Watch API返回的各种事件,LocalStore是Lister的List/Get方法访问。

Kubernetes 核心机制list-watch

List-watch是K8S统一的异步消息处理机制,各组件间协同都采用该机制进行通信。List-watch机制保证了消息的实时性,可靠性,顺序性,性能等等,为声明式风格的API奠定了良好的基础,它是优雅的通信方式,是K8S 架构的精髓。对系统的性能、数据一致性起到关键性的作用。

list-watch操作主要完成以下几个事情:

  • Watch核心数据存储是etcd,是典型的发布-订阅模式。但不直接访问etcd,通过apiserver发起请求,在组件启动时进行订阅。

  • 可以带条件向apiserver发起的watch请求。例如,scheduler想要watch的是所有未被调度的Pod来进行调度操作;而kubelet只关心自己节点上的Pod列表。apiserver向etcd发起的watch是没有条件的,只能知道某个数据发生了变化或创建、删除,但不能过滤具体的值。也就是说对象数据的条件过滤必须在apiserver端而不是etcd端完成。

  • list是watch失败,数据太过陈旧后的弥补手段,这方面详见 基于list-watch的Kubernetes异步事件处理框架详解-客户端部分。list本身是一个简单的列表操作。

Watch 体验,通过curl命令watch pods资源:

[root@xxx-xxx-0-148 xxx]# curl -i http://127.0.0.1:8080/api/v1/watch/services?watch=yes
HTTP/1.1 200 OK
Cache-Control: no-cache, private
Content-Type: application/json
Date: Wed, 16 Jun 2021 09:38:27 GMT
Transfer-Encoding: chunked{"type":"ADDED","object":{"kind":"Service","apiVersion":"v1","metadata":{"name":"coredns","namespace":"kube-system","selfLink":"/api/v1/namespaces/kube-system/services/coredns","uid":"ea75ccb1-fab5-44be-9382-ac36d18e39d9","resourceVersion":"763","creationTimestamp":"2021-06-15T08:29:36Z","labels":{"app":"coredns","k8s-app":"coredns","kubernetes.io/cluster-service":"true","kubernetes.io/name":"CoreDNS","release":"cceaddon-coredns"},"annotations":{"prometheus.io/port":"9153","prometheus.io/scrape":"true"},"managedFields":[{"manager":"Go-http-client","operation":"Update","apiVersion":"v1","time":"2021-06-15T08:29:36Z","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{".":{},"f:prometheus.io/port":{},"f:prometheus.io/scrape":{}},"f:labels":{".":{},"f:app":{},"f:k8s-app":{},"f:kubernetes.io/cluster-service":{},"f:kubernetes.io/name":{},"f:release":{}}},"f:spec":{"f:clusterIP":{},"f:ports":{".":{},"k:{\"port\":53,\"protocol\":\"TCP\"}":{".":{},"f:name":{},"f:port":{},"f:protocol":{},"f:targetPort":{}},"k:{\"port\":53,\"protocol\":\"UDP\"}":{".":{},"f:name":{},"f:port":{},"f:protocol":{},"f:targetPort":{}},"k:{\"port\":8080,\"protocol\":\"TCP\"}":{".":{},"f:name":{},"f:port":{},"f:protocol":{},"f:targetPort":{}}},"f:selector":{".":{},"f:app":{},"f:k8s-app":{}},"f:sessionAffinity":{},"f:type":{}}}}]},"spec":{"ports":[{"name":"dns","protocol":"UDP","port":53,"targetPort":5353},{"name":"dns-tcp","protocol":"TCP","port":53,"targetPort":5353},{"name":"health","protocol":"TCP","port":8080,"targetPort":8080}],"selector":{"app":"coredns","k8s-app":"coredns"},"clusterIP":"10.247.3.10","type":"ClusterIP","sessionAffinity":"None"},"status":{"loadBalancer":{}}}}
{"type":"ADDED","object":{"kind":"Service","apiVersion":"v1","metadata":{"name":"kubernetes","namespace":"default","selfLink":"/api/v1/namespaces/default/services/kubernetes","uid":"99e80360-402a-4368-8710-fa67a2c4a778","resourceVersion":"157","creationTimestamp":"2021-06-15T08:27:36Z","labels":{"component":"apiserver","provider":"kubernetes"},"managedFields":[{"manager":"kube-apiserver","operation":"Update","apiVersion":"v1","time":"2021-06-15T08:27:36Z","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:labels":{".":{},"f:component":{},"f:provider":{}}},"f:spec":{"f:clusterIP":{},"f:ports":{".":{},"k:{\"port\":443,\"protocol\":\"TCP\"}":{".":{},"f:name":{},"f:port":{},"f:protocol":{},"f:targetPort":{}}},"f:sessionAffinity":{},"f:type":{}}}}]},"spec":{"ports":[{"name":"https","protocol":"TCP","port":443,"targetPort":5444}],"clusterIP":"10.247.0.1","type":"ClusterIP","sessionAffinity":"None"},"status":{"loadBalancer":{}}}}

Watch 是如何实现的?

Watch的核心是长链接,通过HTTP 长链接接收apiserver发来的资源变更事件呢,秘诀是Chunked transfer encoding(分块传输编码),它首次出现在HTTP/1.1。

HTTP 分块传输编码允许服务器为动态生成的内容维持 HTTP 持久链接。通常,持久链接需要服务器在开始发送消息体前发送Content-Length消息头字段,但是对于动态生成的内容来说,在内容创建完之前是不可知的。使用分块传输编码,数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。

当客户端调用watch API时,apiserver 在response的HTTP Header中设置Transfer-Encoding的值为chunked,表示采用分块传输编码,客户端收到该信息后,便和服务端该链接,并等待下一个数据块,即资源的事件信息,直到客户主动断链。

List-Watch 的设计理念

一个异步消息的系统时,对消息机制有至少如下四点要求 :

消息可靠性:首先消息必须是可靠的,list和watch一起保证了消息的可靠性,避免因消息丢失而造成状态不一致场景。具体而言,list API可以查询当前的资源及其对应的状态(即期望的状态),客户端通过拿期望的状态和实际的状态进行对比,纠正状态不一致的资源。Watch API和apiserver保持一个长链接,接收资源的状态变更事件并做相应处理。如果仅调用watch API,若某个时间点连接中断,就有可能导致消息丢失,所以需要通过list API解决消息丢失的问题。从另一个角度出发,我们可以认为list API获取全量数据,watch API获取增量数据。虽然仅仅通过轮询list API,也能达到同步资源状态的效果,但是存在开销大,实时性不足的问题。

消息实时性:消息必须是实时的,list-watch机制下,每当apiserver的资源产生状态变更事件,都会将事件及时的推送给客户端,从而保证了消息的实时性。

消息顺序性:消息的顺序性也是非常重要的,在并发的场景下,客户端在短时间内可能会收到同一个资源的多个事件,对于关注最终一致性的K8S来说,它需要知道哪个是最近发生的事件,并保证资源的最终状态如同最近事件所表述的状态一样。K8S在每个资源的事件中都带一个resourceVersion的标签,这个标签是递增的数字,所以当客户端并发处理同一个资源的事件时,它就可以对比resourceVersion来保证最终的状态和最新的事件所期望的状态保持一致。

高性能:List-watch还具有高性能的特点,虽然仅通过周期性调用list API也能达到资源最终一致性的效果,但是周期性频繁的轮询大大的增大了开销,增加apiserver的压力。而watch作为异步消息通知机制,复用一条长链接,保证实时性的同时也保证了性能。

list-watch 实现机制

1) List-watch的API处理, kube-apiserver API注册代码pkg/apiserver/api_installer.go

  • rest.Storage对象会被转换为watcher和lister对象

  • 提供list和watch服务的入口是同一个,在API接口中通过 GET /xxx/services?watch=ture来区分

  • API处理函数是统一通过ListResource完成

func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService) (*metav1.APIResource, error) {
... ...
// what verbs are supported by the storage, used to know what verbs we support per path
creater, isCreater := storage.(rest.Creater)
namedCreater, isNamedCreater := storage.(rest.NamedCreater)
lister, isLister := storage.(rest.Lister)
... ...
watcher, isWatcher := storage.(rest.Watcher)
... ...
case "LIST": // List all resources of a kind.doc := "list objects of kind " + kindif isSubresource {doc = "list " + subresource + " of objects of kind " + kind}handler := metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, requestScope, metrics.APIServerComponent, deprecated, removedRelease, restfulListResource(lister, watcher, reqScope, false, a.minRequestTimeout))if enableWarningHeaders {handler = utilwarning.AddWarningsHandler(handler, warnings)}
... ...

2)ListResource()的具体实现

每次有一个watch的url请求过来,都会调用rw.Watch()创建一个watcher,然后使用serveWatch()来处理这个请求。watcher的生命周期是每个http请求的,这一点非常重要。

func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatch bool, minRequestTimeout time.Duration) http.HandlerFunc {
... ...if opts.Watch || forceWatch {
... ...defer cancel()watcher, err := rw.Watch(ctx, &opts)if err != nil {scope.err(err, w, req)return}requestInfo, _ := request.RequestInfoFrom(ctx)metrics.RecordLongRunning(req, requestInfo, metrics.APIServerComponent, func() {serveWatch(watcher, scope, outputMediaType, req, w, timeout)})return}
... ...
}

3) 响应http请求的过程serveWatch()的代码在/pkg/apiserver/watch.go里面

watcher的结果channel中读取一个event对象,然后持续不断的编码写入到http response的流当中。

// serveWatch will serve a watch response.
// TODO: the functionality in this method and in WatchServer.Serve is not cleanly decoupled.
func serveWatch(watcher watch.Interface, scope *RequestScope, mediaTypeOptions …) {
... ...server.ServeHTTP(w, req)
}
// ServeHTTP serves a series of encoded events via HTTP with Transfer-Encoding: chunked
// or over a websocket connection.
func (s *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
... ...for {select {case event, ok := <-ch:if !ok {// End of results.return}metrics.WatchEvents.WithLabelValues(kind.Group, kind.Version, kind.Kind).Inc()obj := s.Fixup(event.Object)if err := s.EmbeddedEncoder.Encode(obj, buf); err != nil {// unexpected errorutilruntime.HandleError(fmt.Errorf("unable to encode watch object %T: %v", obj, err))return}
... ...         buf.Reset()}}
}

list-watch 实现机制总结

list-watch客户端从调用到响应的整个流程:


感谢您的阅读,也欢迎您发表关于这篇文章的任何建议,关注我,技术不迷茫!

  • 云原生钻石课程 | 第1课:容器运行时技术深度剖析

  • 谈阿里云云效 DevOps | DevOps 不再只是 Jenkins

  • MySQL性能优化(七):MySQL执行计划,真的很重要,来一起学习吧

  • 微服务架构下的核心话题 (三):微服务架构的技术选型

喜欢就点个"在看"呗,留言、转发朋友圈

云原生钻石课程 | 第2课:Kubernetes 技术架构深度剖析相关推荐

  1. 云原生钻石课程 | 第6课:Kubernetes网络架构原理深度剖析(上)

    点击上方"程序猿技术大咖",关注并选择"设为星标" 回复"加群"获取入群讨论资格! 本篇文章来自<华为云云原生王者之路训练营>钻 ...

  2. 云原生钻石课程 | 第5课:Kubernetes存储架构原理深度剖析(下)

    点击上方"程序猿技术大咖",关注并选择"设为星标" 回复"加群"获取入群讨论资格! 本篇文章来自<华为云云原生王者之路训练营>钻 ...

  3. 云原生钻石课程|第8课:Kubernetes运维管理详解(上)

    点击上方"程序猿技术大咖",关注并选择"设为星标" 回复"加群"获取入群讨论资格! 本篇文章来自<华为云云原生王者之路训练营>钻 ...

  4. 云原生钻石课程 | 第1课:容器运行时技术深度剖析

    点击上方"程序猿技术大咖",关注并选择"设为星标" 回复"加群"获取入群讨论资格! 本篇文章来自<华为云云原生王者之路训练营>钻 ...

  5. 【云驻共创】华为云云原生之Istio控制面架构深度剖析

    文章目录 前言 一.Istio的基本概念 1.Istio诞生背景 2.Istio的定义 3.Istio优势 二.Istio整体架构及工作原理 1.Istio整体架构 1.1 控制面Istiod 1.1 ...

  6. 2022阿里云码上公益“第益课”大学生技术公益实践计划活动说明

    简介:简介: 2022年,码上公益联合共青团杭州市余杭区团委.杭州师范大学.浙江工商大学.浙江大学.阿里巴巴公益.阿里云开发者学堂.宜搭发起"第益课"高校技术公益实践计划,为高校学 ...

  7. 直播回顾 | 子芽CCF TF:云原生场景下软件供应链风险治理技术浅谈

    CCF TF(技术前线委员会,Tech Frontier Committee)是中国计算机学会(CCF)为企业界计算机专业人士创建的企业间常态化合作交流平台,创始委员由Intel.LinkedIn.M ...

  8. 18 ubuntu 远程原生_CNCF公布中国云原生调查报告:49%使用容器技术,Kubernetes 应用率达 72%...

    中国云原生使用势头正在增强"作者 | 李菁瑛近日,云原生计算基金会(Cloud Native Computing Foundation,CNCF)发布了第三次中国云原生调查报告(2019年) ...

  9. 福利!百度免费开放内部云原生精品课程

    近年来,云原生的关注度居高不下,全世界的开发者都对由此而兴起的一众技术十分追捧,中国的开发者们也经历了从关注"云原生概念"到关注"云原生落地实践"的转变.在企业 ...

最新文章

  1. js 调用php 导出表格,[拿来主义]使用layui纯JS导出html页面中的table为excel
  2. sortingOrder,sortingLayer
  3. Springboot整合Hikari数据库连接池,密码加密
  4. Android官方开发文档Training系列课程中文版:高效显示位图之加载大位图
  5. 语音基础知识-基本语音知识,声谱图,log梅普图,MFCC,deltas详解
  6. html5简介、选项输入框、表单元素分组、input新增属性及属性值
  7. Bootstrap表格内容垂直水平居中
  8. jquery的html,text,val
  9. 【Python之旅】第四篇(二):Python异常处理与异常捕捉
  10. 看完这篇解决你99%的运维安全陋习,快别踩坑了!
  11. 微信公众号 | 封面图及缩略图设置及修改技巧
  12. 17美亚团队赛电子取证
  13. php把 图片上传到 图片服务器
  14. 网贷平台倒闭了钱要还吗?网贷平台倒闭了怎么还钱
  15. 容齐的身世_白发容齐和容乐是什么关系?容齐和容乐是兄妹吗?
  16. 【TypeError: Descriptors cannot not be created directly. 】解决方法
  17. xp支持python最高版本是多少_PYQT5-开发向下兼容到xp系统的windows软件
  18. python datetime 格式化_[已解决]Python中用strftime格式化datetime出错
  19. python内置库求复数的辐角_根据下列选项,回答 30~34 题: A.杜仲B.黄柏C.厚朴D.肉桂E.牡丹皮 第 30 题 断面较平坦,粉...
  20. 机器人建模----运动学模型及代码实现

热门文章

  1. 魔兽世界服务端centos架设
  2. 加油站收银系统 Java_提升加油站收银效率的收银系统
  3. 计算机毕业设计源码—springboot+vue疫情物资管理系统
  4. datatable 每页显示多少条 放到表格下面
  5. 基于springboot+vue的开源自定义表单问卷系统
  6. activiti6官方示例笔记
  7. MyBatis-Plus调用插入Insert()方法报错
  8. linux杀死线程函数,Linux线程-pthread_kill
  9. oc和java的优点缺点_oc语言的优点和缺点
  10. 计算机毕业设计PHP+安卓电影院售票管理APP论文(源码+程序+lw+远程调试)