精华内容
下载资源
问答
  • 最近升级了新的系统感觉睡眠耗电异常,一路排查到 Windows的事件查看器,发现一直有一个异常“创建 TLS 客户端 凭据时发生严重错误。内部错误状态为 10013。” 网上搜了很多的资料,比较多的是设...

    电脑是 联想小新pro13 2019 i7版本

    版本    Windows 10 家庭版
    版本号    20H2
    安装日期    ‎2019/‎12/‎8
    操作系统版本    19042.572
    体验    Windows Feature Experience Pack 120.2212.31.0
     

    最近升级了新的系统感觉睡眠耗电异常,一路排查到 Windows的事件查看器,发现一直有一个异常“创建 TLS 客户端 凭据时发生严重错误。内部错误状态为 10013。”

    网上搜了很多的资料,比较多的是设置Internet Explorer 11(IE浏览器):设置-高级选项-重置,试过之后发现异常依然存在。之后我卸载了IE,发现异常还是存在,所以问题不是出在IE上。

    最终通过windows 搜索  “Internet属性”,找到“高级”,然后取消使用TLS 1.0, 1.1 (只保留TLS 1.2),随后就发现不再报出异常。算是治好了我的强迫症。

    另外,关于睡眠问题,个人建议是睡眠之前关闭例如微信、QQ、QQ音乐等腾讯系软件,否则会不断唤醒影响电脑睡眠。关闭软件之后发现电脑睡眠正常。

     

    展开全文
  • kubectl 创建 Pod 背后到底发生了什么?想象一下,如果我想将 nginx 部署到 Kubernetes 集群,我可能会在终端中输入类似这样的命令:$ kubectl run --image=nginx --replicas=3然后回车。几秒钟后,你就会看到三个 ...

    kubectl 创建 Pod 背后到底发生了什么?

    想象一下,如果我想将 nginx 部署到 Kubernetes 集群,我可能会在终端中输入类似这样的命令:

    $ kubectl run --image=nginx --replicas=3

    然后回车。几秒钟后,你就会看到三个 nginx pod 分布在所有的工作节点上。这一切就像变魔术一样,但你并不知道这一切的背后究竟发生了什么事情。

    Kubernetes 的神奇之处在于:它可以通过用户友好的 API 来处理跨基础架构的 deployments,而背后的复杂性被隐藏在简单的抽象中。但为了充分理解它为我们提供的价值,我们需要理解它的内部原理。

    本指南将引导您理解从 client 到 Kubelet 的请求的完整生命周期,必要时会通过源代码来说明背后发生了什么。

    这是一份可以在线修改的文档,如果你发现有什么可以改进或重写的,欢迎提供帮助!

    1. kubectl

    验证和生成器

    当敲下回车键以后,kubectl 首先会执行一些客户端验证操作,以确保不合法的请求(例如,创建不支持的资源或使用格式错误的镜像名称)将会快速失败,也不会发送给 kube-apiserver。通过减少不必要的负载来提高系统性能。

    验证通过之后, kubectl 开始将发送给 kube-apiserver 的 HTTP 请求进行封装。kube-apiserver 与 etcd 进行通信,所有尝试访问或更改 Kubernetes 系统状态的请求都会通过 kube-apiserver 进行,kubectl 也不例外。kubectl 使用生成器(generators)来构造 HTTP 请求。生成器是一个用来处理序列化的抽象概念。

    通过 kubectl run 不仅可以运行 deployment,还可以通过指定参数 --generator 来部署其他多种资源类型。如果没有指定 --generator 参数的值,kubectl 将会自动判断资源的类型。

    例如,带有参数 --restart-policy=Always 的资源将被部署为 Deployment,而带有参数 --restart-policy=Never 的资源将被部署为 Pod。同时 kubectl 也会检查是否需要触发其他操作,例如记录命令(用来进行回滚或审计)。

    在 kubectl 判断出要创建一个 Deployment 后,它将使用 DeploymentV1Beta1 生成器从我们提供的参数中生成一个运行时对象。

    API 版本协商与 API 组

    为了更容易地消除字段或者重新组织资源结构,Kubernetes 支持多个 API 版本,每个版本都在不同的 API 路径下,例如 /api/v1 或者 /apis/extensions/v1beta1。不同的 API 版本表明不同的稳定性和支持级别,更详细的描述可以参考 Kubernetes API 概述。

    API 组旨在对类似资源进行分类,以便使得 Kubernetes API 更容易扩展。API 的组名在 REST 路径或者序列化对象的 apiVersion 字段中指定。例如,Deployment 的 API 组名是 apps,最新的 API 版本是 v1beta2,这就是为什么你要在 Deployment manifests 顶部输入 apiVersion: apps/v1beta2

    kubectl 在生成运行时对象后,开始为它找到适当的 API 组和 API 版本,然后组装成一个版本化客户端,该客户端知道资源的各种 REST 语义。该阶段被称为版本协商,kubectl 会扫描 remote API 上的 /apis 路径来检索所有可能的 API 组。由于 kube-apiserver 在 /apis 路径上公开了 OpenAPI 格式的规范文档, 因此客户端很容易找到合适的 API。

    为了提高性能,kubectl 将 OpenAPI 规范缓存到了 ~/.kube/cache 目录。如果你想了解 API 发现的过程,请尝试删除该目录并在运行 kubectl 命令时将 -v 参数的值设为最大值,然后你将会看到所有试图找到这些 API 版本的HTTP 请求。参考 kubectl 备忘单。

    最后一步才是真正地发送 HTTP 请求。一旦请求发送之后获得成功的响应,kubectl 将会根据所需的输出格式打印 success message。

    客户端身份认证

    在发送 HTTP 请求之前还要进行客户端认证,这是之前没有提到的,现在可以来看一下。

    为了能够成功发送请求,kubectl 需要先进行身份认证。用户凭证保存在 kubeconfig 文件中,kubectl 通过以下顺序来找到 kubeconfig 文件:

    • 如果提供了 --kubeconfig 参数, kubectl 就使用 –kubeconfig 参数提供的 kubeconfig 文件。
    • 如果没有提供 –kubeconfig 参数,但设置了环境变量 $KUBECONFIG,则使用该环境变量提供的 kubeconfig 文件。
    • 如果 –kubeconfig 参数和环境变量 $KUBECONFIG 都没有提供,kubectl 就使用默认的 kubeconfig 文件 $HOME/.kube/config

    解析完 kubeconfig 文件后,kubectl 会确定当前要使用的上下文、当前指向的群集以及与当前用户关联的任何认证信息。如果用户提供了额外的参数(例如 –username),则优先使用这些参数覆盖 kubeconfig 中指定的值。一旦拿到这些信息之后, kubectl 就会把这些信息填充到将要发送的 HTTP 请求头中:

    • x509 证书使用 tls.TLSConfig 发送(包括 CA 证书)。
    • bearer tokens 在 HTTP 请求头 Authorization 中发送。
    • 用户名和密码通过 HTTP 基本认证发送。
    • OpenID 认证过程是由用户事先手动处理的,产生一个像 bearer token 一样被发送的 token。

    2. kube-apiserver


    认证

    现在我们的请求已经发送成功了,接下来将会发生什么?这时候就该 kube-apiserver 闪亮登场了!kube-apiserver 是客户端和系统组件用来保存和检索集群状态的主要接口。为了执行相应的功能,kube-apiserver 需要能够验证请求者是合法的,这个过程被称为认证。

    那么 apiserver 如何对请求进行认证呢?当 kube-apiserver 第一次启动时,它会查看用户提供的所有 CLI 参数,并组合成一个合适的令牌列表。

    举个例子:如果提供了 --client-ca-file 参数,则会将 x509 客户端证书认证添加到令牌列表中;如果提供了 --token-auth-file 参数,则会将 breaer token 添加到令牌列表中。

    每次收到请求时,apiserver 都会通过令牌链进行认证,直到某一个认证成功为止:

    • x509 处理程序将验证 HTTP 请求是否是由 CA 根证书签名的 TLS 密钥进行编码的。
    • bearer token 处理程序将验证 --token-auth-file 参数提供的 token 文件是否存在。
    • 基本认证处理程序确保 HTTP 请求的基本认证凭证与本地的状态匹配。

    如果认证失败,则请求失败并返回相应的错误信息;如果验证成功,则将请求中的 Authorization 请求头删除,并将用户信息添加到其上下文中。这给后续的授权和准入控制器提供了访问之前建立的用户身份的能力。

    授权

    OK,现在请求已经发送,并且 kube-apiserver 已经成功验证我们是谁,终于解脱了!

    然而事情并没有结束,虽然我们已经证明了我们是合法的,但我们有权执行此操作吗?毕竟身份和权限不是一回事。为了进行后续的操作,kube-apiserver 还要对用户进行授权。

    kube-apiserver 处理授权的方式与处理身份验证的方式相似:通过 kube-apiserver 的启动参数 --authorization_mode 参数设置。它将组合一系列授权者,这些授权者将针对每个传入的请求进行授权。如果所有授权者都拒绝该请求,则该请求会被禁止响应并且不会再继续响应。如果某个授权者批准了该请求,则请求继续。

    kube-apiserver 目前支持以下几种授权方法:

    • webhook: 它与集群外的 HTTP(S) 服务交互。
    • ABAC: 它执行静态文件中定义的策略。
    • RBAC: 它使用 rbac.authorization.k8s.io API Group实现授权决策,允许管理员通过 Kubernetes API 动态配置策略。
    • Node: 它确保 kubelet 只能访问自己节点上的资源。

    准入控制

    突破了之前所说的认证和授权两道关口之后,客户端的调用请求就能够得到 API Server 的真正响应了吗?答案是:不能!

    从 kube-apiserver 的角度来看,它已经验证了我们的身份并且赋予了相应的权限允许我们继续,但对于 Kubernetes 而言,其他组件对于应不应该允许发生的事情还是很有意见的。所以这个请求还需要通过 Admission Controller 所控制的一个 准入控制链 的层层考验,官方标准的 “关卡” 有近十个之多,而且还能自定义扩展!

    虽然授权的重点是回答用户是否有权限,但准入控制器会拦截请求以确保它符合集群的更广泛的期望和规则。它们是资源对象保存到 etcd 之前的最后一个堡垒,封装了一系列额外的检查以确保操作不会产生意外或负面结果。不同于授权和认证只关心请求的用户和操作,准入控制还处理请求的内容,并且仅对创建、更新、删除或连接(如代理)等有效,而对读操作无效。

    注意
    准入控制器的工作方式与授权者和验证者的工作方式类似,但有一点区别:与验证链和授权链不同,如果某个准入控制器检查不通过,则整个链会中断,整个请求将立即被拒绝并且返回一个错误给终端用户。

    准入控制器设计的重点在于提高可扩展性,某个控制器都作为一个插件存储在 plugin/pkg/admission 目录中,并且与某一个接口相匹配,最后被编译到 kube-apiserver 二进制文件中。

    大部分准入控制器都比较容易理解,接下来着重介绍 SecurityContextDenyResourceQuotaLimitRanger 这三个准入控制器。

    • SecurityContextDeny 该插件将禁止创建设置了 Security Context 的 Pod。
    • ResourceQuota 不仅能限制某个 Namespace 中创建资源的数量,而且能限制某个 Namespace 中被 Pod 所请求的资源总量。该准入控制器和资源对象 ResourceQuota 一起实现了资源配额管理。
    • LimitRanger 作用类似于上面的 ResourceQuota 控制器,针对 Namespace 资源的每个个体(Pod 与 Container 等)的资源配额。该插件和资源对象 LimitRange 一起实现资源配额管理。

    3. etcd


    到现在为止,Kubernetes 已经对该客户端的调用请求进行了全面彻底地审查,并且已经验证通过,运行它进入下一个环节。下一步 kube-apiserver 将对 HTTP 请求进行反序列化,然后利用得到的结果构建运行时对象(有点像 kubectl 生成器的逆过程),并保存到 etcd 中。下面我们将这个过程分解一下。

    当收到请求时,kube-apiserver 是如何知道它该怎么做的呢?事实上,在客户端发送调用请求之前就已经产生了一系列非常复杂的流程。我们就从 kube-apiserver 二进制文件首次运行开始分析吧:

    1. 当运行 kube-apiserver 二进制文件时,它会创建一个允许 apiserver 聚合的服务链。这是一种对 Kubernetes API 进行扩展的方式。
    2. 同时会创建一个 generic apiserver 作为默认的 apiserver。
    3. 然后利用生成的 OpenAPI 规范来填充 apiserver 的配置。
    4. 然后 kube-apiserver 遍历数据结构中指定的所有 API 组,并将每一个 API 组作为通用的存储抽象保存到 etcd 中。当你访问或变更资源状态时,kube-apiserver 就会调用这些 API 组。
    5. 每个 API 组都会遍历它的所有组版本,并且将每个 HTTP 路由映射到 REST 路径中。
    6. 当请求的 METHOD 是 POST 时,kube-apiserver 就会将请求转交给 资源创建处理器。

    现在 kube-apiserver 已经知道了所有的路由及其对应的 REST 路径,以便在请求匹配时知道调用哪些处理器和键值存储。多么机智的设计!现在假设客户端的 HTTP 请求已经被 kube-apiserver 收到了:

    1. 如果处理链可以将请求与已经注册的路由进行匹配,就会将该请求交给注册到该路由的专用处理器来处理;如果没有任何一个路由可以匹配该请求,就会将请求转交给基于路径的处理器(比如当调用 /apis 时);如果没有任何一个基于路径的处理器注册到该路径,请求就会被转交给 not found 处理器,最后返回 404
    2. 幸运的是,我们有一个名为 createHandler 的注册路由!它有什么作用呢?首先它会解码 HTTP 请求并进行基本的验证,例如确保请求提供的 json 与 API 资源的版本相匹配。
    3. 接下来进入审计和准入控制阶段。
    4. 然后资源将会通过 storage provider 保存到 etcd 中。默认情况下保存到 etcd 中的键的格式为 <namespace>/<name>,你也可以自定义。
    5. 资源创建过程中出现的任何错误都会被捕获,最后 storage provider 会执行 get 调用来确认该资源是否被成功创建。如果需要额外的清理工作,就会调用后期创建的处理器和装饰器。
    6. 最后构造 HTTP 响应并返回给客户端。

    原来 apiserver 做了这么多的工作,以前竟然没有发现呢!到目前为止,我们创建的 Deployment 资源已经保存到了 etcd 中,但 apiserver 仍然看不到它。

    4. 初始化


    在一个资源对象被持久化到数据存储之后,apiserver 还无法完全看到或调度它,在此之前还要执行一系列Initializers。Initializers是一种与资源类型相关联的控制器,它会在资源对外可用之前执行某些逻辑。如果某个资源类型没有Initializers,就会跳过此初始化步骤立即使资源对外可见。

    正如大佬的博客指出的那样,Initializers是一个强大的功能,因为它允许我们执行通用引导操作。例如:

    • 将代理边车容器注入到暴露 80 端口的 Pod 中,或者加上特定的 annotation
    • 将保存着测试证书的 volume 注入到特定命名空间的所有 Pod 中。
    • 如果 Secret 中的密码小于 20 个字符,就组织其创建。

    initializerConfiguration 资源对象允许你声明某些资源类型应该运行哪些Initializers。如果你想每创建一个 Pod 时就运行一个自定义Initializers,你可以这样做:

    d84f0de0fae48a5b088e5e67e6facc36.png

    通过该配置创建资源对象 InitializerConfiguration 之后,就会在每个 Pod 的 metadata.initializers.pending 字段中添加 custom-pod-initializer 字段。该初始化控制器会定期扫描新的 Pod,一旦在 Pod 的 pending 字段中检测到自己的名称,就会执行其逻辑,执行完逻辑之后就会将 pending 字段下的自己的名称删除。

    只有在 pending 字段下的列表中的第一个Initializers可以对资源进行操作,当所有的Initializers执行完成,并且 pending 字段为空时,该对象就会被认为初始化成功。

    你可能会注意到一个问题:如果 kube-apiserver 不能显示这些资源,那么用户级控制器是如何处理资源的呢?

    为了解决这个问题,kube-apiserver 暴露了一个 ?includeUninitialized 查询参数,它会返回所有的资源对象(包括未初始化的)。

    5. 控制循环


    Deployments controller

    到了这个阶段,我们的 Deployment 记录已经保存在 etcd 中,并且所有的初始化逻辑都执行完成,接下来的阶段将会涉及到该资源所依赖的拓扑结构。在 Kubernetes 中,Deployment 实际上只是一系列 Replicaset 的集合,而 Replicaset 是一系列 Pod 的集合。那么 Kubernetes 是如何从一个 HTTP 请求按照层级结构依次创建这些资源的呢?其实这些工作都是由 Kubernetes 内置的 Controller(控制器) 来完成的。

    Kubernetes 在整个系统中使用了大量的 Controller,Controller 是一个用于将系统状态从“当前状态”修正到“期望状态”的异步脚本。所有 Controller 都通过 kube-controller-manager 组件并行运行,每种 Controller 都负责一种具体的控制流程。首先介绍一下 Deployment Controller

    将 Deployment 记录存储到 etcd 并初始化后,就可以通过 kube-apiserver 使其可见,然后 Deployment Controller 就会检测到它(它的工作就是负责监听 Deployment 记录的更改)。在我们的例子中,控制器通过一个 Informer 注册一个创建事件的特定回调函数(更多信息参加下文)。

    当 Deployment 第一次对外可见时,该 Controller 就会将该资源对象添加到内部工作队列,然后开始处理这个资源对象:

    通过使用标签选择器查询 kube-apiserver 来检查该 Deployment 是否有与其关联的 ReplicaSetPod 记录。

    有趣的是,这个同步过程是状态不可知的,它核对新记录与核对已经存在的记录采用的是相同的方式。

    在意识到没有与其关联的 ReplicaSetPod 记录后,Deployment Controller 就会开始执行弹性伸缩流程:

    创建 ReplicaSet 资源,为其分配一个标签选择器并将其版本号设置为 1。

    ReplicaSet 的 PodSpec 字段从 Deployment 的 manifest 以及其他相关元数据中复制而来。有时 Deployment 记录在此之后也需要更新(例如,如果设置了 process deadline)。

    当完成以上步骤之后,该 Deployment 的 status 就会被更新,然后重新进入与之前相同的循环,等待 Deployment 与期望的状态相匹配。由于 Deployment Controller 只关心 ReplicaSet,因此需要通过 ReplicaSet Controller 来继续协调。

    ReplicaSets controller

    在前面的步骤中,Deployment Controller 创建了第一个 ReplicaSet,但仍然还是没有 Pod,这时候就该 ReplicaSet Controller 登场了!ReplicaSet Controller 的工作是监视 ReplicaSets 及其相关资源(Pod)的生命周期。和大多数其他 Controller 一样,它通过触发某些事件的处理器来实现此目的。

    当创建 ReplicaSet 时(由 Deployment Controller 创建),RS Controller 检查新 ReplicaSet 的状态,并检查当前状态与期望状态之间存在的偏差,然后通过调整 Pod 的副本数来达到期望的状态。

    Pod 的创建也是批量进行的,从 SlowStartInitialBatchSize 开始,然后在每次成功的迭代中以一种 slow start 操作加倍。这样做的目的是在大量 Pod 启动失败时(例如,由于资源配额),可以减轻 kube-apiserver 被大量不必要的 HTTP 请求吞没的风险。如果创建失败,最好能够优雅地失败,并且对其他的系统组件造成的影响最小!

    Kubernetes 通过 Owner References(在子级资源的某个字段中引用其父级资源的 ID) 来构造严格的资源对象层级结构。这确保了一旦 Controller 管理的资源被删除(级联删除),子资源就会被垃圾收集器删除,同时还为父级资源提供了一种有效的方式来避免他们竞争同一个子级资源(想象两对父母都认为他们拥有同一个孩子的场景)。

    Owner References 的另一个好处是:它是有状态的。如果有任何 Controller 重启了,那么由于资源对象的拓扑关系与 Controller 无关,该操作不会影响到系统的稳定运行。这种对资源隔离的重视也体现在 Controller 本身的设计中:Controller 不能对自己没有明确拥有的资源进行操作,它们应该选择对资源的所有权,互不干涉,互不共享。

    有时系统中也会出现孤儿(orphaned)资源,通常由以下两种途径产生:

    • 父级资源被删除,但子级资源没有被删除
    • 垃圾收集策略禁止删除子级资源

    当发生这种情况时,Controller 将会确保孤儿资源拥有新的 Owner。多个父级资源可以相互竞争同一个孤儿资源,但只有一个会成功(其他父级资源会收到验证错误)。

    Informers

    你可能已经注意到,某些 Controller(例如 RBAC 授权器或 Deployment Controller)需要先检索集群状态然后才能正常运行。拿 RBAC 授权器举例,当请求进入时,授权器会将用户的初始状态缓存下来,然后用它来检索与 etcd 中的用户关联的所有 角色(Role)和 角色绑定(RoleBinding)。那么问题来了,Controller 是如何访问和修改这些资源对象的呢?事实上 Kubernetes 是通过 Informer 机制来解决这个问题的。

    Infomer 是一种模式,它允许 Controller 查找缓存在本地内存中的数据(这份数据由 Informer 自己维护)并列出它们感兴趣的资源。

    虽然 Informer 的设计很抽象,但它在内部实现了大量的对细节的处理逻辑(例如缓存),缓存很重要,因为它不但可以减少对 Kubenetes API 的直接调用,同时也能减少 Server 和 Controller 的大量重复性工作。通过使用 Informer,不同的 Controller 之间以线程安全(Thread safety)的方式进行交互,而不必担心多个线程访问相同的资源时会产生冲突。

    有关 Informer 的更多详细解析,请参考这篇文章:Kubernetes: Controllers, Informers, Reflectors and Stores

    Scheduler

    当所有的 Controller 正常运行后,etcd 中就会保存一个 Deployment、一个 ReplicaSet 和 三个 Pod 资源记录,并且可以通过 kube-apiserver 查看。然而,这些 Pod 资源现在还处于 Pending状态,因为它们还没有被调度到集群中合适的 Node 上运行。这个问题最终要靠调度器(Scheduler)来解决。

    Scheduler 作为一个独立的组件运行在集群控制平面上,工作方式与其他 Controller 相同:监听实际并将系统状态调整到期望的状态。具体来说,Scheduler 的作用是将待调度的 Pod 按照特定的算法和调度策略绑定(Binding)到集群中某个合适的 Node 上,并将绑定信息写入 etcd 中(它会过滤其 PodSpec 中 NodeName 字段为空的 Pod),默认的调度算法的工作方式如下:

    1. 当 Scheduler 启动时,会注册一个默认的预选策略链,这些预选策略会对备选节点进行评估,判断备选节点是否满足备选 Pod 的需求。例如,如果 PodSpec 字段限制了 CPU 和内存资源,那么当备选节点的资源容量不满足备选 Pod 的需求时,备选 Pod 就不会被调度到该节点上(资源容量=备选节点资源总量-节点中已存在 Pod 的所有容器的需求资源(CPU 和内存)的总和
    2. 一旦筛选出符合要求的候选节点,就会采用优选策略计算出每个候选节点的积分,然后对这些候选节点进行排序,积分最高者胜出。例如,为了在整个系统中分摊工作负载,这些优选策略会从备选节点列表中选出资源消耗最小的节点。每个节点通过优选策略时都会算出一个得分,计算各项得分,最终选出分值大的节点作为优选的结果。

    一旦找到了合适的节点,Scheduler 就会创建一个 Binding 对象,该对象的 NameUid 与 Pod 相匹配,并且其 ObjectReference 字段包含所选节点的名称,然后通过 POST 请求发送给 apiserver。

    当 kube-apiserver 接收到此 Binding 对象时,注册吧会将该对象反序列化并更新 Pod 资源中的以下字段:

    • NodeName 的值设置为 ObjectReference 中的 NodeName。
    • 添加相关的注释。
    • PodScheduledstatus 值设置为 True。可以通过 kubectl 来查看:
      $ kubectl get <PODNAME> -o go-template='{{range .status.conditions}}{{if eq .type "PodScheduled"}}{{.status}}{{end}}{{end}}'
      复制

    一旦 Scheduler 将 Pod 调度到某个节点上,该节点的 Kubelet 就会接管该 Pod 并开始部署。

    注意
    预选策略和优选策略都可以通过 –policy-config-file 参数来扩展,如果默认的调度器不满足要求,还可以部署自定义的调度器。如果 podSpec.schedulerName 的值设置为其他的调度器,则 Kubernetes 会将该 Pod 的调度转交给那个调度器。

    6. Kubelet


    Pod 同步

    现在,所有的 Controller 都完成了工作,我们来总结一下:

    • HTTP 请求通过了认证、授权和准入控制阶段。
    • 一个 Deployment、ReplicaSet 和三个 Pod 资源被持久化到 etcd 存储中。
    • 然后运行了一系列Initializers。
    • 最后每个 Pod 都被调度到合适的节点。

    然而到目前为止,所有的状态变化仅仅只是针对保存在 etcd 中的资源记录,接下来的步骤涉及到运行在工作节点之间的 Pod 的分布状况,这是分布式系统(比如 Kubernetes)的关键因素。这些任务都是由 Kubelet 组件完成的,让我们开始吧!

    在 Kubernetes 集群中,每个 Node 节点上都会启动一个 Kubelet 服务进程,该进程用于处理 Scheduler 下发到本节点的任务,管理 Pod 的生命周期,包括挂载卷、容器日志记录、垃圾回收以及其他与 Pod 相关的事件。

    如果换一种思维模式,你可以把 Kubelet 当成一种特殊的 Controller,它每隔 20 秒(可以自定义)向 kube-apiserver 通过 NodeName 获取自身 Node 上所要运行的 Pod 清单。一旦获取到了这个清单,它就会通过与自己的内部缓存进行比较来检测新增加的 Pod,如果有差异,就开始同步 Pod 列表。我们来详细分析一下同步过程:

    1. 如果 Pod 正在创建, Kubelet 就会f="https://github.com/kubernetes/kubernetes/blob/fc8bfe2d8929e11a898c4557f9323c482b5e8842/pkg/kubelet/kubelet.go#L1519">记录一些在 Prometheus 中用于追踪 Pod 启动延时的指标。
    2. 然后生成一个 PodStatus 对象,它表示 Pod 当前阶段的状态。Pod 的状态(Phase) 是 Pod 在其生命周期中的最精简的概要,包括 PendingRunningSucceededFailedUnkown 这几个值。状态的产生过程非常过程,所以很有必要深入了解一下背后的原理:
    • 首先串行执行一系列 Pod 同步处理器(PodSyncHandlers),每个处理器检查检查 Pod 是否应该运行在该节点上。当所有的处理器都认为该 Pod 不应该运行在该节点上,则 Pod 的 Phase 值就会变成 PodFailed,并且将该 Pod 从该节点上驱逐出去。例如当你创建一个 Job 时,如果 Pod 失败重试的时间超过了 spec.activeDeadlineSeconds 设置的值,就会将 Pod 从该节点驱逐出去。
    • 接下来,Pod 的 Phase 值由 init 容器 和应用容器的状态共同来决定。因为目前容器还没有启动,容器被视为处于等待阶段,如果 Pod 中至少有一个容器处于等待阶段,则其 Phase 值为 Pending。
    • 最后,Pod 的 Condition 字段由 Pod 内所有容器的状态决定。现在我们的容器还没有被容器运行时创建,所以 "https://github.com/kubernetes/kubernetes/blob/fc8bfe2d8929e11a898c4557f9323c482b5e8842/pkg/kubelet/status/generate.go#L70-L81">PodReady 的状态被设置为 False。可以通过 kubectl 查看:
      $ kubectl get <PODNAME> -o go-template='{{range .status.conditions}}{{if eq .type "Ready"}}{{.status}}{{end}}{{end}}'
      复制
    1. 生成 PodStatus 之后(Pod 中的 status 字段),Kubelet 就会将它发送到 Pod 的状态管理器,该管理器的任务是通过 apiserver 异步更新 etcd 中的记录。
    2. 接下来运行一系列准入处理器来确保该 Pod 是否具有相应的权限(包括强制执行 "https://github.com/kubernetes/kubernetes/blob/fc8bfe2d8929e11a898c4557f9323c482b5e8842/pkg/kubelet/kubelet.go#L883-L884">AppArmor 配置文件和 NO_NEW_PRIVS),被准入控制器拒绝的 Pod 将一直保持 Pending 状态。
    3. 如果 Kubelet 启动时指定了 cgroups-per-qos 参数,Kubelet 就会为该 Pod 创建 cgroup 并进行相应的资源限制。这是为了更方便地对 Pod 进行服务质量(QoS)管理。
    4. 然后为 Pod 创建相应的目录,包括 Pod 的目录(/var/run/kubelet/pods/<podID>),该 Pod 的卷目录(<podDir>/volumes)和该 Pod 的插件目录(<podDir>/plugins)。
    5. 卷管理器会href="https://github.com/kubernetes/kubernetes/blob/2723e06a251a4ec3ef241397217e73fa782b0b98/pkg/kubelet/volumemanager/volume_manager.go#L330">挂载 Spec.Volumes 中定义的相关数据卷,然后等待是否挂载成功。根据挂载卷类型的不同,某些 Pod 可能需要等待更长的时间(比如 NFS 卷)。
    6. 从 apiserver 中检索 Spec.ImagePullSecrets 中定义的所有 Secret,然后将其注入到容器中。
    7. 最后通过容器运行时接口(Container Runtime Interface(CRI))开始启动容器(下面会详细描述)。

    CRI 与 pause 容器

    到了这个阶段,大量的初始化工作都已经完成,容器已经准备好开始启动了,而容器是由容器运行时(例如 DockerRkt)启动的。

    为了更容易扩展,Kubelet 从 1.5.0 开始通过容器运行时接口与容器运行时(Container Runtime)交互。简而言之,CRI 提供了 Kubelet 和特定的运行时之间的抽象接口,它们之间通过协议缓冲区(它像一个更快的 JSON)和 gRPC API(一种非常适合执行 Kubernetes 操作的 API)。这是一个非常酷的想法,通过使用 Kubelet 和运行时之间定义的契约关系,容器如何编排的具体实现细节已经变得无关紧要。由于不需要修改 Kubernetes 的核心代码,开发者可以以最小的开销添加新的运行时。

    不好意思有点跑题了,让我们继续回到容器启动的阶段。第一次启动 Pod 时,Kubelet 会通过 Remote Procedure Command(RPC) 协议调用 RunPodSandbox。sandbox 用于描述一组容器,例如在 Kubernetes 中它表示的是 Pod。sandbox 是一个很宽泛的概念,所以对于其他没有使用容器的运行时仍然是有意义的(比如在一个基于 hypervisor 的运行时中,sandbox 可能指的就是虚拟机)。

    我们的例子中使用的容器运行时是 Docker,创建 sandbox 时首先创建的是 pause 容器。pause 容器作为同一个 Pod 中所有其他容器的基础容器,它为 Pod 中的每个业务容器提供了大量的 Pod 级别资源,这些资源都是 Linux 命名空间(包括网络命名空间,IPC 命名空间和 PID 命名空间)。

    pause 容器提供了一种方法来管理所有这些命名空间并允许业务容器共享它们,在同一个网络命名空间中的好处是:同一个 Pod 中的容器可以使用 localhost 来相互通信。pause 容器的第二个功能与 PID 命名空间的工作方式相关,在 PID 命名空间中,进程之间形成一个树状结构,一旦某个子进程由于父进程的错误而变成了“孤儿进程”,其便会被 init 进程进行收养并最终回收资源。关于 pause 工作方式的详细信息可以参考:The Almighty Pause Container。

    一旦创建好了 pause 容器,下面就会开始检查磁盘状态然后开始启动业务容器。

    CNI 和 Pod 网络

    现在我们的 Pod 已经有了基本的骨架:一个共享所有命名空间以允许业务容器在同一个 Pod 里进行通信的 pause 容器。但现在还有一个问题,那就是容器的网络是如何建立的?

    当 Kubelet 为 Pod 创建网络时,它会将创建网络的任务交给 CNI 插件。CNI 表示容器网络接口(Container Network Interface),和容器运行时的运行方式类似,它也是一种抽象,允许不同的网络提供商为容器提供不同的网络实现。通过将 json 配置文件(默认在 /etc/cni/net.d 路径下)中的数据传送到相关的 CNI 二进制文件(默认在 /opt/cni/bin 路径下)中,cni 插件可以给 pause 容器配置相关的网络,然后 Pod 中其他的容器都使用 pause 容器的网络。下面是一个简单的示例配置文件:

    515d0863d10a57b0df7ca4471ddfb5ba.png

    CNI 插件还会通过 CNI_ARGS 环境变量为 Pod 指定其他的元数据,包括 Pod 名称和命名空间。

    下面的步骤因 CNI 插件而异,我们以 bridge 插件举例:

    • 该插件首先会在根网络命名空间(也就是宿主机的网络命名空间)中设置本地 Linux 网桥,以便为该主机上的所有容器提供网络服务。
    • 然后它会将一个网络接口(veth 设备对的一端)插入到 pause 容器的网络命名空间中,并将另一端连接到网桥上。你可以这样来理解 veth 设备对:它就像一根很长的管道,一端连接到容器,一端连接到根网络命名空间中,数据包就在管道中进行传播。
    • 接下来 json 文件中指定的 IPAM Plugin 会为 pause 容器的网络接口分配一个 IP 并设置相应的路由,现在 Pod 就有了自己的 IP。
      • IPAM Plugin 的工作方式和 CNI Plugin 类似:通过二进制文件调用并具有标准化的接口,每一个 IPAM Plugin 都必须要确定容器网络接口的 IP、子网以及网关和路由,并将信息返回给 CNI 插件。最常见的 IPAM Plugin 是 host-local,它从预定义的一组地址池中为容器分配 IP 地址。它将地址池的信息以及分配信息保存在主机的文件系统中,从而确保了同一主机上每个容器的 IP 地址的唯一性。
    • 最后 Kubelet 会将集群内部的 DNS 服务器的 Cluster IP 地址传给 CNI 插件,然后 CNI 插件将它们写到容器的 /etc/resolv.conf 文件中。

    一旦完成了上面的步骤,CNI 插件就会将操作的结果以 json 的格式返回给 Kubelet。

    跨主机容器网络

    到目前为止,我们已经描述了容器如何与宿主机进行通信,但跨主机之间的容器如何通信呢?

    通常情况下使用 overlay 网络来进行跨主机容器通信,这是一种动态同步多个主机间路由的方法。 其中最常用的 overlay 网络插件是 flannel,flannel 具体的工作方式可以参考 CoreOS 的文档。

    容器启动

    所有网络都配置完成后,接下来就开始真正启动业务容器了!

    一旦 sanbox 完成初始化并处于 active 状态,Kubelet 就可以开始为其创建容器了。首先启动 PodSpec 中定义的 init 容器,然后再启动业务容器。具体过程如下:

    1. 首先拉取容器的镜像。如果是私有仓库的镜像,就会利用 PodSpec 中指定的 Secret 来拉取该镜像。
    2. 然后通过 CRI 接口创建容器。Kubelet 向 PodSpec 中填充了一个 ContainerConfig 数据结构(在其中定义了命令,镜像,标签,挂载卷,设备,环境变量等待),然后通过 protobufs 发送给 CRI 接口。对于 Docker 来说,它会将这些信息反序列化并填充到自己的配置信息中,然后再发送给 Dockerd 守护进程。在这个过程中,它会将一些元数据标签(例如容器类型,日志路径,dandbox ID 等待)添加到容器中。
    3. 接下来会使用 CPU 管理器来约束容器,这是 Kubelet 1.8 中新添加的 alpha 特性,它使用 UpdateContainerResources CRI 方法将容器分配给本节点上的 CPU 资源池。
    4. 最后容器开始真正启动。
    5. 如果 Pod 中配置了容器生命周期钩子(Hook),容器启动之后就会运行这些 Hook。Hook 的类型包括两种:Exec(执行一段命令) 和 HTTP(发送HTTP请求)。如果 PostStart Hook 启动的时间过长、挂起或者失败,容器将永远不会变成 running 状态。

    7. 总结


    如果上面一切顺利,现在你的集群上应该会运行三个容器,所有的网络,数据卷和秘钥都被通过 CRI 接口添加到容器中并配置成功。

    上文所述的创建 Pod 整个过程的流程图如下所示:

    1bf9bbed3af54f3ee82790748109bb47.png
    展开全文
  • Kubectl 验证和生成器 首先,当我们按下回车执行命令后,Kubectl 会执行客户端验证,以确保非法请求(如创建不支持的资源或使用格式错误的镜像名称)快速失败,并不会发送给 kube-apiserver——即通过减少不必要的...

    8beb633b02e235c8e708dc927c118777.png
    作者:Jamie Hannaford
    翻译:bbbmj(才云)
    校对:bot(才云)
    源代码解释版(强烈建议阅读):https://github.com/bbbmj/what-happens-when-k8s

    想象一下,当你想在 Kubernetes 集群部署 Nginx 时,你会执行以下命令:

    983d061d464ce224b0bda7fdea6a938c.png

    几秒后,你将看到三个 Nginx Pod 分布在集群 Worker 节点上。这相当神奇,但它背后究竟发生了什么?Kubernetes 最为人称道的地方是,它通过用户友好的 API 处理跨基础架构的工作负载部署,通过简单的抽象隐藏其背后的复杂性。但是,为了充分理解它为我们提供的价值,我们需要理解它的原理。本文将带领你充分了解从客户端到 Kubelet 请求的完整生命周期,并在必要时通过源代码解释它到底是什么。

    Kubectl

    验证和生成器

    首先,当我们按下回车执行命令后,Kubectl 会执行客户端验证,以确保非法请求(如创建不支持的资源或使用格式错误的镜像名称)快速失败,并不会发送给 kube-apiserver——即通过减少不必要的负载来提高系统性能

    验证通过后, Kubectl 开始封装它将发送给 kube-apiserver 的 HTTP 请求。在 Kubernetes 中,访问或更改状态的所有尝试都通过 kube-apiserver 进行,后者又与 etcd 进行通信。Kubectl 客户端也不例外。为了构造 HTTP 请求, Kubectl 使用生成器(generators),这是一种负责序列化的抽象

    你可能没有注意到,通过执行 kubectl run,除了运行 Deployment,我们还能利用指定参数 --generator 来部署其他工作负载。

    如果没有指定 --generator 参数的值, Kubectl 会自动推断资源的类型,具体如下:

    • 具有 --restart-policy=Always 的资源被视为 Deployment;
    • 具有 --restart-policy=OnFailure 的资源被视为 Job;
    • 具有 --restart-policy=Never 的资源被视为 Pod。

    Kubectl 还将确定是否需要触发其他操作,例如记录命令(用于部署或审计),或者此命令是否是 dry run。

    当 Kubectl 判断出要创建一个 Deployment 后,它将使用 DeploymentV1Beta1 generator 配合我们提供的参数,生成一个运行时对象(Runtime Object)。

    API Group 和版本协商

    这里值得指出的是, Kubernetes 使用的是一个分类为 API Group 的版本化 API。它旨在对资源进行分类,以便于推理。

    同时,它还为单个 API 提供了更好的版本化方案。例如,Deployment 的 API Group 为 apps,其最新版本为 v1。这也是我们为什么需要在 Deployment manifests 顶部指定 apiVersion: apps/v1 的原因。

    回归正文, Kubectl 生成运行时对象之后,就开始为它查找合适的 API Group 和版本,然后组装一个知道该资源各种 REST 语义的版本化客户端。

    这个发现阶段被称为版本协商(version negotiation),这时 Kubectl 会扫描 remote API 上的 /apis 路径以检索所有可能的 API Group。

    由于 kube-apiserver 在 /apis 路径中公开其 OpenAPI 格式的 scheme 文档,客户端可以借此轻松找到匹配的 API。

    为了提高性能, Kubectl 还将 OpenAPI scheme 缓存到 ~/.kube/cache/discovery 目录。如果要了解 API 发现的完整过程,你可以试着删除该目录并在运行 Kubectl 命令时将 -v 参数的值设为最大,然后你就可以在日志中看到所有试图找到这些 API 版本的 HTTP 请求。最后一步才是真正地发送 HTTP 请求。一旦请求获得成功的响应, Kubectl 将会根据所需的输出格式打印 success message。

    客户端验证

    我们在上文中没有提到的一件事是客户端身份验证(这是在发送 HTTP 请求之前处理的),现在让我们来看看。

    为了成功发送请求, Kubectl 需要先进行身份验证。用户凭据一般存储在 kubeconfig 文件中,但该文件可以存储在其他不同位置。为了定位到它,我们可以执行以下操作:

    • 如果指定参数 --kubeconfig,那么采用该值;
    • 如果指定环境变量 $KUBECONFIG,那么采用该值;
    • 否则查看默认的目录,如 ~/.kube,并使用找到的第一个文件。

    解析文件后,它会确定当前要使用的上下文、当前指向的集群以及当前与用户关联的所有身份验证信息。如果用户提供了额外的参数(例如 --username),则这些值优先,并将覆盖 kubeconfig 中指定的值。

    一旦有了上述信息, Kubectl 就会填充客户端的配置,以便它能够适当地修饰 HTTP 请求:

    • x509 证书使用 tls.TLSConfig 发送(包括 CA 证书);
    • bearer tokens 在 HTTP 请求头 Authorization 中发送;
    • 用户名和密码通过 HTTP 基础认证发送;
    • OpenID 认证过程是由用户事先手动处理的,产生一个像 bearer token 一样被发送的 token。

    9bc29ef4cfadb3e424cf9ede2876591b.png

    kube-apiserver

    认证

    我们的请求已经发送成功,接下来呢?kube-apiserver!

    kube-apiserver 是客户端和系统组件用来持久化和检索集群状态的主要接口。为了执行其功能,它需要能够验证请求是否合法,这个过程被称为认证 (Authentication)

    为了验证请求,当服务器首次启动时,kube-apiserver 会查看用户提供的所有 CLI 参数,并组装合适的 authenticator 列表。

    举个例子:

    • 如果指定参数 --client-ca-file,它会把 x509 authenticator 添加到列表中;
    • 如果指定参数 --token-auth-file,它会把 token authenticator 添加到列表中。

    每次收到请求时,它都会遍历 authenticator 列表进行认证,直到成功为止:

    • x509 handler 会验证 HTTP 请求是否是通过 CA 根证书签名的 TLS 密钥编码的;
    • bearer token handler 会验证 HTTP Authorization header 指定的 token 是否存在于 --token-auth-file 参数提供的 token 文件中;
    • basicauth handler 会简单验证 HTTP 请求的基本身份凭据。

    如果认证失败,则请求失败并返回汇总的错误信息。

    如果成功,则从请求中删除 Authorization 标头,并将用户信息添加到其上下文中,为之后的操作(例如授权和准入控制器)提供访问先前建立的用户身份的能力。

    授权

    请求已发送,kube-apiserver 也已成功验证我们是谁,所以我们终于解脱了???

    想太多!

    虽然我们证明了自己是谁,但还没证明有权执行此操作。毕竟身份(identity)和许可(permission)并不是一回事。因此,kube-apiserver 需要授权。

    kube-apiserver 处理授权的方式与身份验证非常相似:基于 CLI 参数输入,汇集一系列 authorizer,这些 authorizer 将针对每个传入请求运行。如果所有 authorizer 都拒绝该请求,则该请求将导致 Forbidden 响应并不再继续。如果单个 authorizer 被批准,则请求继续。

    Kubernetes v1.14 的 authorizer 实例:

    • webhook:与集群外的 HTTP(S) 服务交互;
    • ABAC:执行静态文件中定义的策略;
    • RBAC:执行由集群管理员添加为 K8s 资源的 RBAC 规则;
    • Node:确保 Kubelet 只能访问自己节点上的资源。

    Admission Controller

    好的,到目前为止,我们已经过认证并获得了 kube-apiserver 的授权。那接下来呢?

    从 kube-apiserver 的角度来看,它已经验证了我们的身份并授权我们执行后续操作,但对于 Kubernetes,系统的其他组件对此还有不少疑义,所以 Admission Controller 该闪亮登场了。

    虽然认证的重点在于证实用户是否具有权限,但是 Admission Controllers 仍会拦截该请求,以确保它符合集群更广泛的期望和规则。它们是对象持久化到 etcd 之前的最后一个堡垒,因此它们封装了剩余的系统检查以确保操作不会产生意外或负面结果。

    Admission Controller 的工作方式类似于 Authentication 和 Authorization,但有一个区别:如果单个 Admission Controller 失败,则整个链断开,请求将失败

    Admission Controller 设计的真正优势在于它致力于提升可扩展性。每个控制器都作为插件存储在 plugin/pkg/admission 目录中,最后编译进 kube-apiserver 二进制文件。

    Kubernetes 目前提供十多种 Admission Controller,此处建议阅读文档:https://v1-14.docs.kubernetes.io/docs/reference/access-authn-authz/admission-controllers/

    15bf8fd491ed5196fdd87906f342f1c4.png

    etcd

    到目前为止, Kubernetes 已经完全审查了传入的请求,并允许它继续往下走。在下一步中,kube-apiserver 将反序列化 HTTP 请求,构造运行时对象(有点像 Kubectl generator 的逆过程),并将它们持久化到 etcd。这里我们稍微分析一下。kube-apiserver 是怎么知道在接受我们的请求时该怎么做的呢?在提供任何请求之前,kube-apiserver 会发生一系列非常复杂的步骤。让我们从第一次运行 kube-apiserver 二进制文件开始:

    • 当运行 kube-apiserver 二进制文件时,它会创建一个服务链,允许 apiserver 聚合。这是一种支持多 apiserver 的方式;
    • 之后,它会创建一个用作默认实现的 generic apiserver;
    • 使用生成的 OpenAPI scheme 填充 apiserver 配置;
    • 然后,kube-apiserver 遍历 scheme 中指定的所有 API Group, 并为其构造 storage provider。当你访问或变更资源状态时, kube-apiserver 就会调用这些 API Group;
    • 对于每个 API Group, 它还会迭代每个组版本,并为每个 HTTP 路由安装 REST 映射。这允许 kube-apiserver 映射请求,并且一旦找到匹配就能够委托给正确的代码逻辑;
    • 对于本文的特定用例,将注册一个 POST handler,该处理程序将委托给 create resource handler。

    现在,kube-apiserver 已经知道存在哪些路由及内部映射,当请求匹配时,它可以调用相应的处理程序和存储程序。这是非常完美的设计模式。让我们假设 HTTP 请求已经被kube-apiserver 收到了:

    • 如果程序处理链可以将请求与注册的路由匹配,它会将该请求交给注册到该路由的 dedicated handler。否则它会回退到 path-based handler(这是调用 /apis 时会发生的情况)。如果没有为该路由注册处理程序,则会调用 not found handler,最终返回 404
    • 幸运的是,我们有一个处理器名为 createHandler!它有什么作用?它将首先解码 HTTP 请求并执行基础验证,例如确保请求提供的 JSON 与我们的版本化 API 资源匹配;
    • 审计和准入控制阶段;
    • 之后,资源会通过 storage provider 存储到 etcd 中。默认情况下,保持到 etcd 的键的格式为 <namespace>/<name>,当然,它也支持自定义;
    • 资源创建过程中出现的任何错误都会被捕获,最后 storage provider 会执行 get 调用来确认该资源是否被成功创建。如果需要额外的清理工作 (finalization),就会调用后期创建的处理器和装饰器;
    • 最后,构造 HTTP 响应并返回给客户端。

    这么多步骤,能够坚持走到这里是非常了不起的!同时,apiserver 实际上也做了很多工作。总结一下:我们部署的 Deployment 现在存在于 etcd 中,但仍没有看到它真正地 work…注:在 Kubernetes v1.14 之前,这往后还有 Initializer 的步骤,该步骤在 v1.14 被 webhook admission 取代。

    控制循环

    Deployment Controller

    截至目前,我们的 Deployment 已经存储于 etcd 中,并且所有的初始化逻辑都已完成。接下来的阶段将涉及 Deployment 所依赖的资源拓扑结构。

    在 Kubernetes, Deployment 实际上只是 ReplicaSet 的集合,而 ReplicaSet 是 Pod 的集合。那么 Kubernetes 如何从一个 HTTP 请求创建这个层次结构呢?这就不得不提 Kubernetes 的内置控制器 (Controller)。

    Kubernetes 系统中使用了大量的 Controller, Controller 是一个用于将系统状态从当前状态调谐到期望状态的异步脚本。所有内置的 Controller 都通过组件 kube-controller-manager 并行运行,每种 Controller 都负责一种具体的控制流程。

    首先,我们介绍一下 Deployment Controller:

    将 Deployment 存储到 etcd 后,我们可以通过 kube-apiserver 使其可见。当这个新资源可用时, Deployment controller 会检测到它,它的工作是监听 Deployment 的更改。在我们的例子中, Controller 通过注册创建事件的回调函数(更多相关信息,参见下文)。

    当我们的 Deployment 首次可用时,将执行此回调函数,并将该对象添加到内部工作队列(internal work queue)。

    当它处理我们的 Deployment 对象时,控制器将检查我们的 Deployment 并意识到没有与之关联的 ReplicaSet 或 Pod。它通过使用标签选择器(label selectors)查询 kube-apiserver 来实现此功能。有趣的是,这个同步过程是状态不可知的。另外,它以相同的方式调谐新对象和已存在的对象。在意识到没有与其关联的 ReplicaSet 或 Pod 后,Deployment Controller 就会开始执行弹性伸缩流程(scaling process)。它通过推出(如创建)一个 ReplicaSet, 为其分配 label selector 并将其版本号设置为 1。ReplicaSet 的 PodSpec 字段是从 Deployment 的 manifest 以及其他相关元数据中复制而来。有时 Deployment 在此之后也需要更新(例如,如果设置了 process deadline)。当完成以上步骤之后,该 Deployment 的 status 就会被更新,然后重新进入与之前相同的循环,等待 Deployment 与期望的状态相匹配。由于 Deployment Controller 只关心 ReplicaSet, 因此需要 ReplicaSet Controller 继续调谐过程。

    ReplicaSet Controller

    在上一步中,Deployment 控制器创建了属于该 Deployment 的第一个 ReplicaSet, 但仍然没有创建 Pod。所以这里我们要引入一个新东西:ReplicaSet 控制器!

    ReplicaSet 控制器的作用是监视 ReplicaSet 及其相关资源 Pod 的生命周期。与大多数其它控制器一样,它通过触发某些事件的处理程序来实现目标。

    当创建 ReplicaSet 时(由 Deployment 控制器创建),ReplicaSet 控制器会检查新 ReplicaSet 的状态,并意识到现有状态与期望状态之间存在偏差。然后,它会尝试通过调整 Pod 的副本数来调谐这种状态。

    Pod 的创建也是批量进行的,从数量 SlowStartInitialBatchSize 开始,然后在每次成功的迭代中以一种 slow start 操作加倍。这样做的目的是在大量 Pod 启动失败时(如由于资源配额),可以减轻 kube-apiserver 被大量不必要的 HTTP 请求吞没的风险。

    Kubernetes 通过 Owner References (子资源的某个字段中引用其父资源的 ID) 来执行严格的资源对象层级结构。这确保了 Controller 管理的资源被删除(级联删除)时,子资源就会被垃圾收集器删除。同时,它还为父资源提供了一种有效的方式来避免竞争同一个子资源(想象两对父母认为他们拥有同一个孩子的场景)。

    Owner References 的另一个好处是,它是有状态的。如果重启任何的 Controller,那么由于资源对象的拓扑关系与 Controller 无关,该重启时间不会影响到系统的稳定运行。这种对资源隔离的重视也体现在 Controller 本身的设计中:Controller 不能对自己没有明确拥有的资源进行操作,它们之间互不干涉,互不共享

    有时系统中也会出现孤儿 (orphaned) 资源,通常由以下两种途径产生:

    • 父资源被删除,但子资源没有被删除;
    • 垃圾收集策略禁止删除子资源。

    当发生这种情况时, Controller 将会确保孤儿资源拥有新的 Owner。多个父资源可以相互竞争同一个孤儿资源,但只有一个会成功(其他父资源会收到一个验证错误)。

    Informers

    你可能已经注意到,有些 Controller(例如 RBAC 授权器或 Deployment Controller)需要检索集群状态然后才能正常运行。

    以 RBAC 授权器为例,当请求进入时,授权器会将用户的初始状态缓存下来供以后使用,然后用它来检索与 etcd 中的用户关联的所有角色(Role)角色绑定(RoleBinding)

    那么 Controller 是如何访问和修改这些资源对象的呢?答案是引入 Informer。

    Infomer 是一种模式,它允许 Controller 订阅存储事件并列出它们感兴趣的资源。除了提供一个很好的工作抽象,它还需要处理很多细节,如缓存。通过使用这种设计,它还允许控制器以线程安全的方式进行交互,而不必担心线程冲突。

    有关 Informer 的更多信息,可深入阅读:http://borismattijssen.github.io/articles/kubernetes-informers-controllers-reflectors-stores

    Scheduler

    当所有的 Controller 正常运行后,etcd 中就会保存一个 Deployment、一个 ReplicaSet 和 三个 Pod, 并且可以通过 kube-apiserver 查看到。然而,这些 Pod 还处于Pending状态,因为它们还没有被调度到集群中合适的 Node 上。最终解决这个问题的 Controller 是 Scheduler。Scheduler 作为一个独立的组件运行在集群控制平面上,工作方式与其他 Controller 相同:监听事件并调谐状态。具体来说, Scheduler 的作用是过滤 PodSpec 中 NodeName 字段为空的 Pod 并尝试将其调度到合适的节点。为了找到合适的节点, Scheduler 会使用特定的算法,默认调度算法工作流程如下:

    • 当 Scheduler 启动时,会注册一系列默认的预选策略,这些预选策略会对候选节点进行评估,判断候选节点是否满足候选 Pod 的需求。例如,如果 PodSpec 显式地限制了 CPU 和内存资源,并且节点的资源容量不满足候选 Pod 的需求时,Pod 就不会被调度到该节点上(资源容量 = 节点资源总量 - 节点中已运行的容器需求资源总和);
    • 一旦选择了适当的节点,就会对剩余的节点运行一系列优先级函数,以对候选节点进行打分。例如,为了在整个系统中分散工作负载,它将偏好于资源请求较少的节点(因为这表明运行的工作负载较少)。当它运行这些函数时,它为每个节点分配一个成绩,然后选择分数最高的节点进行调度。

    一旦算法找到了合适的节点, Scheduler 就会创建一个 Binding 对象,该对象的 Name 和 Uid 与 Pod 相匹配,并且其 ObjectReference 字段包含所选节点的名称,然后通过发送 POST 请求给 apiserver。当 kube-apiserver 接收到此 Binding 对象时,注册表会将该对象反序列化(registry deserializes)并更新 Pod 资源中的以下字段:

    • 将 NodeName 的值设置为 Binding 对象 ObjectReference 中的 NodeName;
    • 添加相关的注释(annotations);
    • 将 PodScheduled 的 status 设置为 True。

    一旦 Scheduler 将 Pod 调度到某个节点上,该节点的 Kubelet 就会接管该 Pod 并开始部署。附注:自定义调度器:有趣的是预测和优先级函数 (predicates and priority functions) 都是可扩展的,可以使用 --policy-config-file 标志来定义。这引入了一定程度的灵活性。管理员还可以在独立部署中运行自定义调度器(具有自定义处理逻辑的控制器)。如果 PodSpec 中包含 schedulerName,Kubernetes 会将该 pod 的调度移交给使用该名称注册的调度器。

    Kubelet

    Pod Sync

    截至目前,所有的 Controller 都完成了工作,让我们来总结一下:

    • HTTP 请求通过了认证、授权和准入控制阶段;
    • 一个 Deployment、ReplicaSet 和三个 Pod 被持久化到 etcd;
    • 最后每个 Pod 都被调度到合适的节点。

    然而,到目前为止,所有的状态变化仅仅只是针对保存在 etcd 中的资源对象,接下来的步骤涉及到在 Worker 节点之间运行具体的容器,这是分布式系统 Kubernetes 的关键因素。这些事情都是由 Kubelet 完成的。

    在 Kubernetes 集群中,每个 Node 节点上都会启动一个 Kubelet 服务进程,该进程用于处理 Scheduler 下发到本节点的任务,管理 Pod 的生命周期。这意味着它将处理 Pod 与 Container Runtime 之间所有的转换逻辑,包括挂载卷、容器日志、垃圾回收以及其他重要事件。

    一个有用的方法,你可以把 Kubelet 当成一种特殊的 Controller,它每隔 20 秒(可以自定义)向 kube-apiserver 查询 Pod,过滤 NodeName 与自身所在节点匹配的 Pod 列表。

    一旦获取到了这个列表,它就会通过与自己的内部缓存进行比较来检测差异,如果有差异,就开始同步 Pod 列表。我们来看看同步过程是什么样的:

    • 如果 Pod 正在创建, Kubelet 就会暴露一些指标,可以用于在 Prometheus 中追踪 Pod 启动延时;
    • 然后,生成一个 PodStatus 对象,表示 Pod 当前阶段的状态。Pod 的 Phase 状态是 Pod 在其生命周期中的高度概括,包括 PendingRunningSucceededFailedUnkown 这几个值。状态的产生过程非常复杂,因此很有必要深入深挖一下:
      • 首先,串行执行一系列 PodSyncHandlers,每个处理器检查 Pod 是否应该运行在该节点上。当其中之一的处理器认为该 Pod 不应该运行在该节点上,则 Pod 的 Phase 值就会变成 PodFailed 并将从该节点被驱逐。例如,以 Job 为例,当一个 Pod 失败重试的时间超过了 activeDeadlineSeconds 设置的值,就会将该 Pod 从该节点驱逐出去
      • 接下来,Pod 的 Phase 值由 init 容器和主容器状态共同决定。由于主容器尚未启动,容器被视为处于等待阶段,如果 Pod 中至少有一个容器处于等待阶段,则其 Phase 值为 Pending
      • 最后,Pod 的 Condition 字段由 Pod 内所有容器状态决定。现在我们的容器还没有被容器运行时 (Container Runtime) 创建,所以,Kubelet 将 PodReady 的状态设置为 False
    • 生成 PodStatus 之后,Kubelet 就会将它发送到 Pod 的 status 管理器,该管理器的任务是通过 apiserver 异步更新 etcd 中的记录;
    • 接下来运行一系列 admit handlers 以确保该 Pod 具有正确的权限(包括强制执行 AppArmor profiles 和 NO_NEW_PRIVS),在该阶段被拒绝的 Pod 将永久处于 Pending 状态;
    • 如果 Kubelet 启动时指定了 cgroups-per-qos 参数,Kubelet 就会为该 Pod 创建 cgroup 并设置对应的资源限制。这是为了更好的 Pod 服务质量(QoS);
    • 为 Pod 创建相应的数据目录,包括:
      • Pod 目录 (通常是 /var/run/kubelet/pods/<podID>)
      • Pod 的挂载卷目录 (<podDir>/volumes)
      • Pod 的插件目录 (<podDir>/plugins)
    • 卷管理器会挂载 Spec.Volumes 中定义的相关数据卷,然后等待挂载成功;
    • 从 apiserver 中检索 Spec.ImagePullSecrets,然后将对应的 Secret 注入到容器中;
    • 最后,通过容器运行时 (Container Runtime) 启动容器(下面会详细描述)。

    CRI 和 pause 容器

    到了这个阶段,大量的初始化工作都已经完成,容器已经准备好开始启动了,而容器是由容器运行时(例如 Docker) 启动的。

    为了更具可扩展性, Kubelet 使用 CRI 来与具体的容器运行时进行交互。简而言之, CRI 提供了 Kubelet 和特定容器运行时实现之间的抽象。通过 protocol buffers(一种更快的 JSON) 和 gRPC API(一种非常适合执行 Kubernetes 操作的API)进行通信。

    这是一个非常酷的想法,因为通过在 Kubelet 和容器运行时之间使用已定义的接口约定,容器编排的实际实现细节变得无关紧要。重要的是接口约定,这允许以最小的开销添加新的容器运行时,因为没有核心 Kubernetes 代码需要更改

    回到部署我们的容器,当一个 Pod 首次启动时, Kubelet 调用 RunPodSandbox 远程过程命令。沙箱是描述一组容器的 CRI 术语,在 Kubernetes 中对应的是 Pod。这个术语是故意模糊的,因此其他不使用容器的运行时,不会失去其意义(想象一个基于 hypervisor 的运行时,沙箱可能指的是 VM)。

    在我们的例子中,我们使用的是 Docker,所以在此容器运行时中,创建沙箱涉及创建 pause 容器。

    pause 容器像 Pod 中的所有其他容器的父级一样,因为它承载了工作负载容器最终将使用的许多 Pod 级资源。这些“资源”是 Linux Namespaces(IPC、Network、PID)。

    pause 容器提供了一种托管所有这些 Namespaces 的方法,并允许子容器共享它们。成为同一 Network Namespace 一部分的好处是同一个 Pod 中的容器可以使用 localhost 相互访问

    pause 容器的第二个好处与 PID Namespace 有关。在这些 Namespace 中,进程形成一个分层树,顶部的“init” 进程负责“收获”僵尸进程。更多信息,请深入阅读:https://www.ianlewis.org/en/almighty-pause-container

    创建 pause 容器后,将开始检查磁盘状态然后启动主容器。

    CNI 和 Pod 网络

    现在,我们的 Pod 有了基本的骨架:一个 pause 容器,它托管所有 Namespaces 以允许 Pod 间通信。但容器的网络如何运作以及建立的?

    当 Kubelet 为 Pod 设置网络时,它会将任务委托给 CNI (Container Network Interface) 插件,其运行方式与 Container Runtime Interface 类似。简而言之,CNI 是一种抽象,允许不同的网络提供商对容器使用不同的网络实现

    Kubelet 通过 stdin 将 JSON 数据(配置文件位于 /etc/cni/net.d 中)传输到相关的 CNI 二进制文件(位于 /opt/cni/bin) 中与之交互。下面是 JSON 配置文件的一个简单示例 :

    730b052c123af707691f0e97a994a987.png

    CNI 插件还可以通过 CNI_ARGS 环境变量为 Pod 指定其他的元数据,包括 Pod Name 和 Namespace。

    接下来会发生什么取决于 CNI 插件,这里我们以 bridge CNI 插件为例:

    • 该插件首先会在 Root Network Namespace(也就是宿主机的 Network Namespace) 中设置本地 Linux 网桥,以便为该主机上的所有容器提供网络服务;
    • 然后将一个网络接口 (veth 设备对的一端)插入到 pause 容器的 Network Namespace 中,并将另一端连接到网桥上。你可以这样来理解 veth 设备对:它就像一根很长的管道,一端连接到容器,一端连接到 Root Network Namespace 中,允许数据包在中间传输;
    • 之后, pause 容器的网络接口分配一个 IP 并设置相应的路由,于是 Pod 就有了自己的 IP。IP 的分配是由 JSON 配置文件中指定的 IPAM Plugin 实现的;
      • IPAM Plugin 的工作方式和 CNI 插件类似:通过二进制文件调用并具有标准化的接口,每一个 IPAM Plugin 都必须要确定容器网络接口的 IP、子网以及网关和路由,并将信息返回给 CNI 插件。最常见的 IPAM Plugin 称为 host-local,它从预定义的一组地址池为容器分配 IP 地址。它将相关信息保存在主机的文件系统中,从而确保了单个主机上每个容器 IP 地址的唯一性。
    • 对于 DNS, Kubelet 将为 CNI 插件指定 Kubernetes 集群内部 DNS 服务器 IP 地址,确保正确设置容器的 resolv.conf 文件。

    跨主机容器网络

    到目前为止,我们已经描述了容器如何与宿主机进行通信,但跨主机之间的容器如何通信呢?

    通常情况下, Kubernetes 使用 Overlay 网络来进行跨主机容器通信,这是一种动态同步多个主机间路由的方法。一个较常用的 Overlay 网络插件是 flannel,它提供了跨节点的三层网络。

    flannel 不会管容器与宿主机之间的通信(这是 CNI 插件的职责),但它对主机间的流量传输负责。为此,它为主机选择一个子网并将其注册到 etcd,然后保留集群路由的本地表示,并将传出的数据包封装在 UDP 数据报中,确保它到达正确的主机。

    更多信息,请深入阅读:

    https://github.com/coreos/flannel

    启动容器

    所有的网络配置都已完成。还剩什么?真正启动工作负载容器!一旦sanbox 完成初始化并处于 active 状态, Kubelet 将开始为其创建容器。首先启动 PodSpec 中定义的 Init Container,然后再启动主容器,具体过程如下:

    • 拉取容器的镜像。如果是私有仓库的镜像,就会使用 PodSpec 中指定的 Secret 来拉取该镜像;
    • 通过 CRI 创建容器。Kubelet 使用 PodSpec 中的信息填充了 ContainerConfig 数据结构(在其中定义了 command、image、labels、mounts、devices、environment variables 等),然后通过 protobufs 发送给 CRI。对于 Docker 来说,它会将这些信息反序列化并填充到自己的配置信息中,然后再发送给 Dockerd 守护进程。在这个过程中,它会将一些元数据(例如容器类型、日志路径、sandbox ID 等)添加到容器中;
    • 然后 Kubelet 将容器注册到 CPU 管理器,它通过使用 UpdateContainerResources CRI 方法给容器分配给本地节点上的 CPU 资源;
    • 最后容器真正启动
    • 如果 Pod 中包含 Container Lifecycle Hooks,容器启动之后就会运行这些 Hooks。Hook 的类型包括两种:Exec(执行一段命令) 和 HTTP(发送 HTTP 请求)。如果 PostStart Hook 启动的时间过长、挂起或者失败,容器将永远不会变成 Running 状态。

    Wrap-up

    最后的最后,现在我们的集群上应该会运行三个容器,分布在一个或多个工作节点上。所有的网络、数据卷和秘钥都由 Kubelet 填充,并通过 CRI 接口添加到容器中,配置成功!

    展开全文
  • 1. 背景 ...然而在实际生产过程中,...HTTPS 的主要作用是在不安全的网络上创建一个基于 TLS/SSL 协议安全信道,对窃听和中间人攻击提供一定程度的合理防护。TLS/SSL 握手的基本流程如下图描述: 3. 案例分...

    1. 背景

     

    HTTPS 作为站点安全的最佳实践之一,已经得到了最广泛的支持。然而在实际生产过程中,由 TLS/SSL 握手失败引起的连接异常问题依然十分常见。本文将结合 mPaaS 客户端实际排查案例,介绍这类问题在移动领域的排查和解决方案。

     

    2. TLS/SSL 握手基本流程

     

    HTTPS 的主要作用是在不安全的网络上创建一个基于 TLS/SSL 协议安全信道,对窃听和中间人攻击提供一定程度的合理防护。TLS/SSL 握手的基本流程如下图描述:

    image

     

     

    3. 案例分享

     

    2.1 CFCA 证书的历史问题

     

    2.1.1 背景

     

    某客户为其生产环境的站点申请了一张由 CFCA 签发的证书。相关域名正确配置该证书且启用 HTTPS 后,经测试发现他们的客户端 App 在低版本手机上( iOS < 10.0,Android < 6.0)无法连接到相关站点。
    客户端调试发现,控制台会看到证书无效的错误信息(Invalid CertificateCertificate Unknown )。

     

    2.1.2 排查

     

    起初,工程师并不知道客户的证书是由哪个机构签发以及有什么问题。而对于这类问题,一般均需要客户端网络包做进一步的分析与判断。因此安排客户在受影响的设备上进行问题复现及客户端抓包操作。

     

    1. 获取到网络包后,首先确认了客户端连接失败的直接原因为 TLS 握手过程异常终止,见下:
      image
    1. 查看 Encrypted Alert 内容,错误信息为 0x02 0x2E。根据 TLS 1.2 协议(RFC5246 )的定义, 该错误为因为 certificate_unknown
    1. 继续查看该证书的具体信息,根据 Server Hello 帧中携带的证书信息得知该证书由证书机构 China Financial Certification Authority(CFCA) 签发。再根据证书信息中的 Authority Information Access (AIA) 信息确认 Intermediate CA 和 Root CA 证书。确认该证书签发机构的根证书为 CFCA EV ROOT
    1. 回到存在问题的手机设备上(Android 5.1),检查系统内置的受信任 CA 根证书列表,未能找到 CFCA EV ROOT CA 证书;而在正常连接的手机上,可以找到该 CA 的根证书并默认设置为”信任“。
    1. 查阅 CFCA 证书的相关说明,该机构的证书在 iOS 10.1 及 Android 6.0 及以上版本才完成入根接入,参考这里:

      image

     

    2.1.3 小结

     

    从上面的分析可以看到,该问题的根因是低版本客户端设备没有内置 CFCA 的 CA 根证书。因此,基本的解决方案包括:

     

    1. 更换其他 CA 机构签发的证书,保证其 CA 根证书的在特定设备上已默认信任。
    1. 手动在受影响的设备上安装该 CA 根证书及中间证书,并配置为信任状态。
    1. 客户端 App 预置该 CA 根证书,并通过客户端代码配置信任该证书。

     

    需要结合不同的业务场景选择合理解决方案。

     

    2.2 证书链信任模式引起的问题

     

    背景

     

    某客户新增了一个容灾备用接入地址,启用了一个新的域名并配置了一张全新的证书。测试发现,切换到该备用地址时,Android 客户端无法正常连接,报证书未知错误(Certificate Unknown);iOS 客户端表现正常。

     

    排查

     

    和 2.1 的问题类似,首先在受影响的设备上进行问题复现及客户端抓包操作。

     

    1. 获取到网络包之后,确认了客户端连接失败的直接原因为 TLS 握手过程异常终止,原因与 2.1 中的问题一样,为Certificate Unknown
      image
    1. 类似问题 2.1 的排查动作,查看该证书的 CA 根证书及根证书的信任情况。
      发现该证书由中间 CA 机构 Secure Site Pro CA G2 签发,其根 CA 为 DigiCert Global Root CA:
      image

      image

    1. DigiCert Global Root CA 作为一个广泛支持的证书签发机构,其根 CA 证书在绝大多数的设备上均为受信任状态,这一点在受影响的设备上也得到了确认。既然根 CA 的证书处于信任状态,为何证书验证还是失败?这成为下一步排查的重点方向。
    1. 同一台设备,切换到正常环境下,也完成一次抓包操作。获取到新的网络包后做对比分析,发现两种情况下网络包中体现的区别为:
      • 正常环境下,服务器返回的证书包含了完整的 CA 证书链;
      • 而异常情况下,服务端返回的证书仅包含叶节点 CA 证书。
        image
        image
    1. 根据上述线索进行排查研究,发现:不同于其他平台,Android 客户端默认是不会通过 AIA Extension 去做证书链的校验( AIA 机制参考这里)。因此,当中间 CA 证书未安装或未缓存时,客户端 App 是不会主动拉取中间 CA 证书并做进一步信任链校验的,参考这里,从而导致证书校验失败。

     

    小结

     

    从上面的排查分析看到,该问题和 Android 平台自身的证书校验机制和证书打包方式相关。解决方案包括:

     

    1. 代码层面手动定制 TrustManager 去定制校验过程;
    1. 或重新打包证书,将中间 CA 证书和根 CA 证书一同打包到服务端证书中。

     

    该客户综合开发成本与环境现状,选择重新打包证书。新的证书配置完成后,问题得到解决。

     

    2.3 加密套件协商引起的问题

     

    背景

     

    某客户反馈他们的 iOS 客户端 App 用户在特定运营商网络环境下无法打开特定的业务站点(HTTPS 站点)。客户端处于白屏等待状态并最终报错;而在同样的网络环境下,系统浏览器可以打开该站点;同一台设备,切换到另一个网络运营商下,也可以访问该站点。

     

    排查

     

    1. 由于该问题直接表现在 Web 层,因此首先尝试通过 Charles 抓取 HTTP 层包进行分析。HTTP 日志发现相关 HTTP 请求并未发出。
    1. 由此怀疑问题发生在 TCP 层,进而在受影响的设备上进行问题复现及客户端抓包操作。
    1. 获取到网络包后,首先确认问题:
      a. 通过页面域名在网络包中寻找 DNS 解析结果;
      b. 根据 DNS 解析结果找到站点 IP,并过滤出客户端与该 IP 之间的访问情况;
      c. 观察客户端与该服务器之间的网络活动,发现存在 TLS 握手失败的情况:
      image
    1. 从上面的网络包可以看到,服务端(机房 P 中的服务器提供接入服务)在收到 Client Hello 后,直接返回了 Handshake Failure,这种情况下,一般需要服务端配合排查握手失败的直接原因。在客户端条件下,可以进一步缩小排查疑点。
    1. 重新考虑客户问题条件:相同的网络条件下,系统浏览器可以打开该页面;同一设备切换到另一运营商下(站点此时由机房 Q 中的服务器提供接入服务),可以正常访问。针对这这两种正常情况进行抓包和进一步分析。
    1. 通过对三种情况的网络观察发现:
      a. 问题 App 发出的 Client Hello 显示支持 17 种加密套件:

      image


      b. 正常 App 发出的 Client Hello 显示支持 26 种加密套件:

      image


      c. 正常 App 和机房 P 服务器协商的加密套件为:TLS_RAS_WITH_3DES_EDE_CBC_SHA (0x000a) (不在问题 App 支持的加密套件范围内);
      d. 问题 App 和机房 Q 服务器协商的加密套件为:TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)(在问题 App 支持的加密套件范围内);
    1. 根据上述情况,可以推论问题的基本情况为:
      • 问题 App 发出去的握手请求,支持17种加密套件( A 集合);
      • 正常 App 发出去的握手请求,支持26种加密套件( B 集合);
      • 机房 P 的接入服务器,能支持 B 集合种的至少一种加密套件,不支持 A 集合中的所有加密套件;
      • 机房 Q 的接入服务器,既支持 A 集合中的至少一种加密套件,也支持 B 集合中的至少一种加密套件;
      • 最终导致 问题 App 无法通过 机房 P 中的服务器 访问该站点。

     

    小结

     

    从上面的分析结论可以看到,由于客户端和服务端加密套件不匹配,导致在特定情况下的握手失败。进一步的问题解决方案包括:

     

    1. 调整客户端加密套件,增加支持的 Cipher Suites(涉及客户端底层 TLS/SSL 库的升级);
    1. 调整服务端加密套件,增加支持的 Cipher Suites(涉及服务端 TLS/SSL 接入配置)。

     

    该客户最终选择调整服务端加密套件,问题得到解决。

     

    3. 总结

     

    从上述案例的分享和实践中可以看到,TLS 层面的问题在客户端的症状表现上有相似之处,但是问题的根因却大相径庭。这里例举的问题虽不能覆盖所有的问题场景,但可以看到基本的排查思路如下:

     

    1. 判断问题是否属于 TLS/SSL 层面的问题。
    1. 抓取网络包;有条件的情况下,可以针对正常和异常情况抓取两份网络包,以便后续进行对比分析。
    1. 根据网络包探寻问题发生的直接原因,进而进一步探究问题的根本原因。
    1. 根据分析结论并结合业务场景,选择合适的解决方案。

     

    这类问题的排查基础是对 HTTPS 和 TLS/SSL 协议的理解以及对分析工具的掌握。在移动领域,这类问题存在一定的共性,直接了解上述结论和分析方法可以帮助开发者快速“出坑”。

     

    参考

     

    展开全文
  • 然而在实际生产过程中,由 TLS/SSL 握手失败引起的连接异常问题依然...2. TLS/SSL 握手基本流程HTTPS 的主要作用是在不安全的网络上创建一个基于 TLS/SSL 协议安全信道,对窃听和中间人攻击提供一定程度的合理防护。...
  • 作为一项重大更改,默认情况下,MQTT.js客户端中内置了一个错误处理程序,因此,如果发出任何错误并且用户尚未在客户端创建错误的事件处理程序,则客户端不会因未处理而中断错误。 另外,已将典型的TLS错误(例如...
  • TLS的配置,包括禁用TLS(纯文本) 所有标量类型的输入生成 嵌套消息的输入生成 枚举(包括嵌套)的输入生成 重复字段的输入生成 oneof和map字段的输入生成 支持添加RPC元数据 执行一元请求 执行服务器流请求 执行...
  • 创建一个新的WebDriver会话。 可以将以下选项指定为方法参数的名称/值对: http_proxy_url => $ url 会话的浏览器使用的HTTP代理的URL。 该值必须是一个对象,其scheme为http并且host和port标识代理服务器。 ...
  • 一. Kubectl① kubectl 所有选项--alsologtostderr[=false]: 同时输出日志到标准错误控制台和文件。--api-version="": 和服务端交互使用的API版本。...--client-certificate="": TLS使用的客户端证书路径。--clien...
  • 单击上面的紫色Deploy to Heroku ,会转移到heroku应用程序创建页面,填上应用程序的名字,选择列表,按需修改部分参数和AUUID后单击下一步部署应用程序立即开始部​​署如出现错误,可以多尝试几次,待部署完成后...
  • 无论为何目的而创建, 该链接都已成功完成任务. 1001 | CLOSE_GOING_AWAY | 终端离开, 可能因为服务端错误, 也可能因为浏览器正从打开连接的页面跳转离开. 1002 | CLOSE_PROTOCOL_ERROR | 由于协议错误而中断连接. ...
  • mp:军警WIF3-源码

    2021-03-07 13:04:57
    通过SSL和TLS传输使用LOGIN,PLAIN,NTLM,CRAM-MD5和Google的XOAUTH2机制进行SMTP身份验证 47种语言的错误消息! DKIM和S / MIME签名支持 与PHP 5.0及更高版本兼容 多得多! 为什么可能需要它 许多PHP开发人员在其...
  • 1.客户端trojan的 config.json { "run_type": "client", "local_addr": "127.0.0.1", "local_port": 1080, "remote_addr": "www.fofsmile.tk", "remote_port": 443, "password": [ "cc" ], "log...
  • qemu-4.0.1.tar.xz

    2019-12-28 00:33:35
    QEMU 4.0.0 发布了,此版本更新亮点包括: ARM:实现了一批 ARMv8.X...Network Block Device:改进跟踪与错误诊断,qemu-nbd 新选项 –bitmap、–list 与 –tls-authz virtio-blk 现在支持 DISCARD 和 WRITE_ZEROES
  • JAVA_API1.6文档(中文)

    万次下载 热门讨论 2010-04-12 13:31:34
    javax.rmi.ssl 通过安全套接字层 (SSL) 或传输层安全 (TLS) 协议提供 RMIClientSocketFactory 和 RMIServerSocketFactory 的实现。 javax.security.auth 此包提供用于进行验证和授权的框架。 javax.security.auth....
  • sip RFC3261 中文版

    2010-10-09 19:09:42
    26.4.3 TLS 292 26.4.4 SIPS URI 293 26.5 Privacy(隐私) 295 27 IANA 认证 295 27.1 Option Tags 296 27.2 Warn-Codes 296 27.3 头域名 297 27.4 方法和应答码 297 27.6 新Content-Disposition 参数注册 299 28 同...
  • 2.1. 使用 virt-install 创建客户端 .................................................................................. 7 2.2. 使用 virt-manager 创建客户端 ..................................................
  • CuteFTP9简易汉化版

    2014-04-11 12:31:30
    SSL会话Choices-When设置SSL连接,一种上传软件允许您选择三种常见的SSL实现,包括TLS(AUTH TLS)*,SSL隐* *(直接连接在端口990)和SSL显式* *(身份验证SSL)模式。大多数FTP服务器支持至少一种,而一些(比如Globalscape ...
  • KArchive:用于创建,读写和操作文件档案(例如zip和 tar)的库,它通过QIODevice的一系列子类,使用gzip格式,提供了透明的压缩和解压缩的数据。 libarchive:多格式的存档和压缩库。 LZ4 :非常快速的压缩算法。...
  • java api最新7.0

    千次下载 热门讨论 2013-10-26 17:34:06
    javax.rmi.ssl 通过安全套接字层 (SSL) 或传输层安全 (TLS) 协议提供 RMIClientSocketFactory 和 RMIServerSocketFactory 的实现。 javax.security.auth 此包提供用于进行验证和授权的框架。 javax.security.auth....
  • javax.rmi.ssl 通过安全套接字层 (SSL) 或传输层安全 (TLS) 协议提供 RMIClientSocketFactory 和 RMIServerSocketFactory 的实现。 javax.security.auth 此包提供用于进行验证和授权的框架。 javax.security.auth....
  • JDK_1_6 API

    2017-03-04 23:06:21
    javax.rmi.ssl 通过安全套接字层 (SSL) 或传输层安全 (TLS) 协议提供 RMIClientSocketFactory 和 RMIServerSocketFactory 的实现。 javax.security.auth 此包提供用于进行验证和授权的框架。 javax.security.auth....
  • [Java参考文档]

    2013-03-19 16:56:15
    javax.rmi.ssl 通过安全套接字层 (SSL) 或传输层安全 (TLS) 协议提供 RMIClientSocketFactory 和 RMIServerSocketFactory 的实现。 javax.security.auth 此包提供用于进行验证和授权的框架。 javax.security.auth....
  • JavaAPI1.6中文chm文档 part1

    热门讨论 2011-08-19 08:33:34
    javax.rmi.ssl 通过安全套接字层 (SSL) 或传输层安全 (TLS) 协议提供 RMIClientSocketFactory 和 RMIServerSocketFactory 的实现。 javax.security.auth 此包提供用于进行验证和授权的框架。 javax.security.auth....
  • JavaAPI中文chm文档 part2

    2011-08-19 08:58:42
    javax.rmi.ssl 通过安全套接字层 (SSL) 或传输层安全 (TLS) 协议提供 RMIClientSocketFactory 和 RMIServerSocketFactory 的实现。 javax.security.auth 此包提供用于进行验证和授权的框架。 javax.security.auth....
  • java jdk-api-1.6 中文 chmd

    2018-03-22 11:32:15
    javax.management.remote.rmi RMI 连接器是供 JMX Remote API 使用的一种连接器,后者使用 RMI 将客户端请求传输到远程 MBean 服务器。 javax.management.timer 提供对 Timer MBean(计时器 MBean)的定义。 javax...
  • Java 1.6 API 中文 New

    2013-10-26 14:08:22
    javax.rmi.ssl 通过安全套接字层 (SSL) 或传输层安全 (TLS) 协议提供 RMIClientSocketFactory 和 RMIServerSocketFactory 的实现。 javax.security.auth 此包提供用于进行验证和授权的框架。 javax.security.auth....

空空如也

空空如也

1 2
收藏数 37
精华内容 14
关键字:

创建tls客户端错误