写一个自己用的小程序,通常是运行它、得到结果,然后关掉。如果想让别人随时打开网页查看读书笔记,程序就得一直运行,等着接收请求。
我们先假设做了这样一个读书笔记服务:用户可以保存笔记,也可以上传配图。它提供读取、保存笔记的接口,后面把这部分程序简称为笔记 API。最初只有几十个人使用,把代码复制到一台服务器,装好依赖,再启动程序,就能开始提供服务了。这里运行中的程序,也就是一个进程。
接下来的问题是:以后修改代码、换服务器,或者程序突然退出,怎样让大家还能够继续使用?云原生关心的,正是程序交给用户以后这些持续发生的事情。下面沿着这个服务一步步看,先从一次更新失败说起。
换一台机器,为什么同一段代码出错了
笔记服务增加了生成图片缩略图的功能。开发机上测试正常,更新到服务器以后,文字笔记还能打开,上传图片却一直报错。排查后发现,开发机装了一个图像处理库,服务器没有。
先在服务器补装这个库,眼前的问题就解决了。不过,下次换服务器,还得记得再装一遍。时间久了,机器上装过哪些库、改过什么配置,可能只存在于维护者的记忆里。
可以把应用代码、需要的库和默认启动方式一起打包,得到一份镜像。镜像里记录了启动应用需要的一部分文件和设置。换机器时,取来同一份镜像,就不用每次都手工凑齐这些文件。
Docker 是常用的镜像和容器工具。它根据镜像准备文件环境,再启动应用进程。这个带有隔离环境、可以单独启动和停止的运行单位,叫作容器。先记住这层关系就够了:镜像是准备好的内容,容器是用这些内容运行起来的程序及其环境。一份镜像可以启动多个容器。Docker 容器入门
回到刚才的事故,把需要的图像处理库放进镜像,能减少“开发机有、服务器没有”的差异。下一步还要把构建镜像的过程写下来,让别人也能照着相同的步骤得到所需内容。第三篇会具体介绍怎样写这些构建步骤。
镜像能带走的内容也有范围。例如,数据库的网络地址、访问密码,通常在运行时提供;运行容器的机器仍要提供操作系统内核,也就是负责进程、文件和网络等工作的底层部分。镜像中的程序还必须支持目标机器的 CPU 架构。换机器后,文件一致、网络可达、配置正确这些条件要分别检查。
更新程序时,用户上传的图片怎么办
有了镜像,更新程序可以按一个固定过程来做:构建新镜像,用它启动新容器,确认能正常提供服务,再替换旧容器。
这时要留意图片放在哪里。假设用户上传的图片只保存在旧容器自己的目录中,删除旧容器,再从新镜像创建一个容器,新容器就拿不到那些图片。新镜像包含的是新版本程序,用户运行期间上传的文件需要另外保存。
因此,笔记服务需要把几样东西分开安排:
- 应用代码和图像处理库放进镜像,随程序版本一起更新
- 数据库地址等配置在启动时提供,测试环境和正式环境可以各用自己的值
- 笔记内容保存在数据库中;正式图片可以交给对象存储,也就是专门保存、读取这类文件的存储服务
这里先抓住一个很具体的要求:换掉处理请求的程序之后,用户保存的内容还在。 为此,数据库和图片的保存位置要独立管理,备份也要单独准备。仅仅把文件放到别处,还不足以应对误删或存储设备损坏。
后面讲容器文件系统时,还会区分停止后再启动原来的容器,与删除旧容器再创建一个新容器。这两种操作对文件的影响不同。
打开一篇笔记,请求会经过哪里
现在考虑程序突然退出的情况。只运行一份笔记 API 时,这份程序停了,用户就暂时读不到笔记。可以同时运行两份,让入口把请求分给它们。其中一份退出后,在故障被发现、请求转发规则更新后,另一份仍有机会继续处理请求。
运行中的每一份应用,叫一个实例。这里两份程序做同样的工作,也可以说 API 有两个副本。它们都查询同一个笔记数据库、访问同一份图片存储,所以用户不需要知道这次请求由哪一份处理。剩下的实例还要有足够容量;如果两份都在同一台机器上,机器故障仍会同时影响它们。
这样再看一次读取笔记的过程,就比较清楚了:
- 浏览器根据域名找到服务的网络入口
- 入口把请求转交给当前可用的一个 API 实例
- API 查询数据库,取出对应笔记,返回给浏览器
- 页面需要显示配图时,浏览器按提供的图片地址继续读取对象存储
沿着这条路径,也能看出谁负责什么:入口负责把请求送过去,应用代码负责查询和返回正确的笔记,数据库和对象存储负责保存数据。容器为应用准备运行环境。出了问题,要沿着这些步骤逐段找原因。
比如页面一直在等,可能是 API 处理太慢,也可能是数据库连接已经用完。后者指的是:已有程序占用了数据库允许的连接,新请求只能继续排队。此时再加几个 API 副本,还可能带来更多连接竞争。先查出时间花在哪里,才能决定该优化查询、限制请求,还是增加某一部分的容量。
要做到这一点,服务需要留下记录。记录请求何时开始、哪一步报错,是日志的工作;统计请求量、耗时和错误数,是监控能提供的线索。再复杂一点,可以把同一次请求在不同程序中的记录串起来,这就是后面会讲的追踪。
如果之后增加了自动生成摘要的功能,也可以让后台程序慢慢处理。API 把待处理的笔记放进队列,队列保存等待领取的任务,后台程序领取后生成摘要。后台程序也需要记录运行版本,并纳入发布步骤,否则页面已经更新,它却可能还在执行旧代码。这里先知道它是一条额外的处理路径就可以了。
把运行实例与持久数据分开,升级代码时才不必一起移动用户数据。
少了一份运行中的程序,谁来补上
假设刚才手动启动了两个 API 实例,十分钟后其中一个退出。另一份可以暂时承担工作,但如果希望一直保有两份,就还需要有人发现缺口,再启动一个新的实例。
可以让程序替我们反复做这件事:记录“我要两份”的目标,观察现在有几份,发现少了就尝试补上,之后继续观察。负责这种检查和调整的程序,叫作控制器。这个反复接近目标的过程,叫作持续协调。Kubernetes 控制器说明
它和执行一次启动脚本的区别,发生在启动之后。脚本完成了那次操作;持续协调还会在后面继续发现变化、作出调整。
持续协调重复检查目标条件;重建程序本身不会修复错误的配置或业务逻辑。
当应用分布在多台机器上,还要决定新实例放在哪台机器、那台机器的资源是否足够。Kubernetes 是管理这类容器应用的平台,常缩写为 K8s。它把一组机器组织成集群,集群里的机器叫节点,并由不同组件配合完成安排位置、观察状态、替换实例等工作。这类集中管理通常叫容器编排。Kubernetes 概述
使用 Kubernetes 时,我们会在配置文件里写明:使用哪份镜像、希望有几个副本、运行需要哪些资源。平台读取这些要求,持续尝试让实际运行情况接近它们。这种先描述目标状态的做法,叫作声明式管理。
对笔记服务而言,想要维持的条件已经很具体:运行指定版本;保有足够的可用副本;新实例准备好后再接收请求;替换实例时保留用户数据。每一项还需要对应的配置、应用配合和验证,后续章节会逐步补齐。
自动补实例也需要时间和可用资源。若服务器没有剩余容量,新的实例就可能起不来。若提供给程序的数据库配置写错了,新实例仍然会连接失败,需要修正配置后再运行。摘要内容是否正确、数据库更新是否损坏了笔记内容,则需要应用测试和数据检查来判断。
这些做法为什么叫云原生
回头看,笔记服务的变化都有实际原因:换机器容易漏装依赖,于是把应用所需内容做成镜像;更新程序可能丢文件,于是让用户数据独立保存;运行中的程序会退出,于是安排故障发现和恢复;请求会变慢,于是收集记录来定位原因。
这些做法合起来,形成了一种构建和运行软件的思路:把应用怎样构建、怎样运行写清楚,尽量让重复工作自动完成,同时保留观察和处理故障的办法。这样,环境变化或发布新版本时,维护者有一套可以重复执行、可以检查结果的过程。
CNCF,也就是云原生计算基金会,在云原生定义中强调了自动化,以及系统的可管理、可观察和应对变化的能力。容器和声明式管理是其中常见的实现方式。我们前面从笔记服务出发,正是在理解这些能力为什么需要、怎样逐步建立。
这些方法可以用于租来的云服务器,也可以用于自己的机房。云服务提供商解决了一部分机器和基础设施问题,应用代码、发布过程、数据保护和故障判断仍需要有人负责。具体分工取决于使用的平台提供了哪些能力。
现在就需要 Kubernetes 吗
假设笔记服务只有一个维护者,每天几百次请求,每月更新一次,停机几分钟也可以接受。这时先把镜像构建稳定、自动部署做好,加上进程退出后的重启、健康检查和数据备份,就已经能解决很多问题。
自己维护 Kubernetes 集群会带来额外工作,例如机器间的网络、访问权限、证书和平台升级。需要比较的是:这些工作花掉的时间,与它替你省下的手工部署、实例管理和故障处理时间。
如果团队已经维护几十个服务,经常发布新版本,需要把很多实例安排到多台机器,统一的平台可能更划算。也可以使用托管平台,由服务商承担约定范围内的管理工作,再根据文档确认自己仍需负责什么。
学习云原生,可以从自己的程序遇到的一个具体问题开始。这个合集继续使用读书笔记服务:下一篇先拆开容器,看看里面的进程、文件和网络是怎么安排的;再下一篇把应用做成镜像。理解这些之后,再进入 Kubernetes 的具体操作。
选读动手:准备一个可以放心重建的实验环境
这一节供准备开始实验时使用。第一次阅读可以先跳过,等读到容器和 Kubernetes 的具体操作时再回来。实验只使用测试数据,与日常工作和生产环境隔开。
这里沿用 Docker、kind 和 kubectl 这条路线,三者分工如下:
- Docker:提供本地构建镜像、运行容器的能力,按 Docker 安装说明准备
- kind:用本机的容器模拟 Kubernetes 节点,方便创建和删除学习用集群,按 kind 快速开始准备
- kubectl:向 Kubernetes 发送命令、查看结果的命令行工具,按 kubectl 安装说明选择对应操作系统和 CPU 架构的版本
先确认这些工具能运行,再创建一个名字明确的实验集群。下面只列验证和创建步骤,安装命令按各自文档及你的系统选择:
docker version
kind version
kubectl version --client
kind create cluster --name cloudnative-lab --wait 120s
kubectl --context kind-cloudnative-lab cluster-info
kubectl --context kind-cloudnative-lab get nodes
context 表示 kubectl 使用哪组集群地址和身份信息。上面的命令显式选择 kind-cloudnative-lab,避免把查看或后续修改操作发到另一个集群。动手前先运行 kubectl config current-context 看看默认选择的是谁。共享机器上还应使用单独的 kubeconfig,也就是 kubectl 连接配置文件,避免混入生产环境凭据。
创建完成后,检查两项结果:cluster-info 能访问集群管理接口;get nodes 显示节点状态为 Ready。这说明节点已具备继续实验的基本条件。再记录实际使用的 kind、节点镜像、Kubernetes 和 kubectl 版本;需要固定实验版本时,从 kind 发布说明选择对应节点镜像,避免随意组合版本。
后面第一轮实验只会用到几个基本对象:Pod 把需要一起运行的容器组织起来;Deployment 管理一组应用副本和更新;Service 为访问这些副本提供稳定入口。第四、五篇会具体解释,现在不用提前配置全部组件。访问实验服务时,也会先使用仅绑定本机地址的端口转发,把本机端口接到实验服务上。
kind 很适合本地学习和测试。云厂商的负载均衡、不同机房之间的故障以及真实存储行为,还需要在相应环境里验证。生产集群需要另行规划高可用、升级、证书、容量与备份,本地实验只覆盖其中一部分。
实验结束后,可以删除明确命名的测试集群:
kind delete cluster --name cloudnative-lab
执行前确认这个集群里只有可丢弃的数据,需要保留的文件先导出。后面的练习都遵循同样的原则,使用专门准备的测试数据。
动手想一想
题目 1
笔记 API 在开发机上能生成缩略图,部署到服务器后,因为缺少图像处理库而失败。现在已经把这个库装进镜像,换机器运行时,还需要检查什么?
提示
先想想哪些内容跟着镜像走,哪些条件由运行它的机器和外部服务提供。
答案
检查镜像中的程序是否支持目标 CPU 架构和操作系统,机器的内核与容器运行工具是否满足要求;再检查启动配置、数据库地址与凭据、网络连通性,以及需要使用的外部文件和访问权限。镜像带上图像库后,解决了缺文件的问题,其他运行条件仍需要核对。
题目 2
两个 API 副本都因为数据库连接数耗尽而超时。把副本数改成十个,为什么可能让问题更严重?
提示
每个副本都会连接数据库。可以假设每个副本最多使用二十个连接,再算一算总数。
答案
两个副本最多需要四十个连接,十个副本则可能需要二百个。数据库能处理的请求没有因此增加,反而可能有更多请求等待连接。应先检查连接怎样分配和释放、哪些查询太慢、请求排队多久,再决定是减少同时处理的请求、优化查询,还是增加数据库容量。

