Report of the Week [1] – Ready for Failure

/ 1评 / 0

Hello, Kubernetes

这周主要的工作是熟悉 Kubernetes. 我寻思着现在全栈程序员的标杆都是这么高了么,顺便还得会 DevOps 可真的刺激。

只要理解 Kubernetes 是来分布式地调度容器的,你只需要告诉 Kubernetes 你要这个系统变成什么样子,Kubernetes 就会自己把所需要做的工作做好,一切问题都会变得明晰起来。让我们以设计师的身份来思考分布式容器调度的各项性质,解决我们可能遇到的问题,就能很快地理解 Kubernetes 的设计。

分布式

分布式,意味着 Kubernetes 需要在高于本机的层面管理容器。所以机器本身也需要被管理。每个机器的配置可能不同,比如它们可能有不同的 CPU 架构、不同的内存量、不同的吞吐、不同的磁盘配置,这些异构状态应该可以被反应出来。这些内容对应 Kubernetes 的 Nodes - Master 和 Worker, 以及 Tag.

但有些时候我们可能需要指定在实在不行的情况下,把服务降级部署到能力有限的节点上。节点标签是一种硬指标,我们需要一个软指标才能表述和实现这样一个需求。这对应了 Kubernetes 的 Node Taint 和 Tolerance. 通过指定合适的 Taint 和 Tolerance, 镜像可以在没有理想候选的情况下部署到备选节点。

由于网络是不可靠的、机器是不可靠的,在调度网络中的节点可能随时掉线和重连。当工作节点掉线的时候,它仍然可以继续服务而无法通知控制节点。所以,需要在工作节点上部署工具监控网络状态和进行实际的镜像调度。当节点和集群失联时,应该自动销毁服务;调度节点也应该及时将该节点的任务迁移到其他节点上。这对应 Kubernets 中的 Kubelet.

容器调度

在本地,我们通常不会手动地启动一个个镜像、设置卷、设置端口。我们会使用 docker-compose 来批量地管理这些容器。一个 docker-compose.yml 代表了一组需要在本机运行的任务。我们也可以在一个机器上运行多组任务。

在 Kubernetes 中,这个概念也被继承了下来——一组容器运行的空间叫做一个 Pod, 一个节点可以运行多组 Pod. 和 docker-compose 不同,Kubernetes 的行为要更细一些。实际上一个 docker-compose.yml 对应的思路更像是 Kubernetes 的 Deployment. 但这要更复杂一些:

假如我们有一个开发中的镜像,我们需要在这个镜像更新的时候在服务器上的容器也跟着更新。如果我们指定一个 Depolyment, 那么 Kubernetes 将会自己拉取对应的镜像,并把它部署到合适的 Pod 上。如果之前这个 Depolyment 已经存在,那么 Kuberenetes 会自动按照新的文件将集群的状态调整到 Depolyment 指定的状态,比如使用给定版本的镜像、使用给定的环境配置等。

在使用 docker-compose 时,要更新容器,我们需要停止这个任务并重新启动这个任务。Kubernetes 也会停止 Pod 并启动新的 Pod. 但不同的是,Kubernetes 可以不一次停止所有的 Pod. 有些时候,我们需要一直保持服务可用,同时进行更新。这个操作可用通过启动多个相同任务到不同的(逻辑)实例,并配置合适的流量转发规则将流量导向可以提供服务的实例实现。Kubernetes 则将启动多个逻辑实例 (Replica) 和流量均衡都内置地实现了。

但我们的多个实例可能是有状态的。有很多程序,比如数据库,是严格有状态的。如果随意启动和停止很有可能导致状态丢失。以及,如果我们启动多个数据库实例,我们将难以保证数据库之间状态一致——剩下的数据库实例需要被配置成主要数据库实例的备份,并且在主数据库失败时,应该可以顶替其并对外服务。这也意味着这些实例有严格的启动顺序——先启动主实例,再启动备份实例。这个需求则由 ConfigMap 和 StatefulSet 实现。

在使用 docker-compose 时,我们有时会使用 volumes 标签指定文件系统的映射。这个在 Kuberenets 有更细的控制:PersistentVolume 和 PersistentVolumeClaim. 前者负责声明节点上存在的存储空间,包括这个存储空间的具体实现;后者负责将存储空间分配到给定的服务。需要意识到,后者申领的空间可以比前者声明的空间要小。这使得我们得以精细地控制各个服务所使用的资源,同时实现统一的文件系统管理。

分布式容器调度

在单机使用 docker 时,我们只需要操心在本机网络上路由容器之间的流量即可。所以在 docker-compose 中,只需要一个 ports 标签就可以指定容器需要暴露的端口。docker-compose 还同时对镜像提供了 DNS 服务,辅助容器之间的通信。

但是在分布式环境下,目标容器可能并不在本地,而在另一个实例上。这时候只进行本地的路由就已经不够了,我们需要进行实例之间的路由。这时候,我们需要某种方式声明容器暴露的端口的存在。Pod 的 IP 地址并不是一成不变的,所以直接写在配置文件里可能会出问题。我们需要更灵活的方式声明服务的存在,而且也要考虑到负载均衡等等。

这种声明方法就是 Service. 通过 Service, 我们可以声明存在于其他节点的容器所提供服务,也可以将容器的端口暴露给主机网络并提供服务。

随时挂掉的 gRPC

当然,如果光是瞪着 Kubernetes 文档看未免也太无聊。我是一个 practical daydreamer, 所以也得干点 practical 的事情。

除了 Kubernetes, 还有一个有趣的问题需要解决:gRPC 其实不是严格无状态的。也就是,它不像典型的 HTTP 服务器(尽管是基于 HTTP/2 的),如果客户端在没啥请求的情况下重启一下服务器、更新一些东西是完全可以做的;gRPC 客户端和服务端之间实际上会保持一个连接,如果重启服务端,那么客户端的连接会暴毙。如果就单纯地用的 Node 的 gRPC 实现,那么这个连接是不会在服务端重新上线之后重连的,而且整个应用会卡死,尤其是如果你顺便指定了退出时向服务端发送数据,那么应用就会直接卡死(因为捕捉了 SIGINTSIGTERM),只能强 kill.

所以,怎么解决呢?答案有些粗暴:监控客户端连接状态,一旦变更到错误状态,就直接销毁客户端并尝试重连。重连失败一定次数后,认定服务失败,清理退出。(毕竟上游依赖都没了,不退出等着过年?)这种暴力的手段,实现起来还算可以

等待磁盘与网络

上班最愉快的事情是合法摸鱼。但是当你工作半天做不完的时候就很焦虑。程序员的合法摸鱼时间就是等待磁盘和网络,而我的不少时间也自然是花费在这上面。

docker build 是我运行过的最让人焦虑的命令。于是我把它扔到了 Jenkins 上让国外节点运行。这个命令的速度是上来了,但是 docker push 的速度就下去了,真的就是构建 5 分钟,推送 1 小时。

啥时候才能摸到 Local Testing Server 啊……

  1. 小生化说道:

    。。。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Your comments will be submitted to a human moderator and will only be shown publicly after approval. The moderator reserves the full right to not approve any comment without reason. Please be civil.