前面的文章已经完成了环境部署,甚至都谈不上走马观花,因为还没有展现 Istio 流量治理的神奇之处,这里先不着急触碰最终的目的,而是回到一个比较基础的问题,示例应用是如何在 K8s 中运行的,这有助于站在一个更坚实的基础上了解 Istio 如何完成增强,就好像揭开了一层朦胧的面纱。
示例应用的架构
上一篇中 Kiali 的拓扑功能清晰展现了 book-info 应用的部署架构和访问拓扑,并不复杂,它是一个微服务架构的应用,有四个服务:
- productpage , Python 应用,调用 details 和 reviews 服务生成书籍产品界面;
- details , Ruby 应用,提供书籍详情;
- reviews , Java 应用,提供书籍评论,有三个版本同时提供服务:
- v1 ,不调用 ratings 服务;
- v2 ,调用 ratings 服务,以黑色五角星显示评分信息;
- v3 ,调用 ratings 服务,以红色五角星显示评分信息;
- ratings , Node 应用,提供书籍评分信息;
由于 reviews 服务的三个版本同时提供服务,所以在刷新 productpage 服务提供的页面时会看到不同的情况,这里展示一个页面示例:
部署到 K8s
如果在传统环境中部署示例应用,仅应用最少需要运行6个进程,因为它是微服务,就必须考虑水平扩展和服务雪崩的问题,必须引入相关的治理组件,所以从运维管理的角度看,微服务引入了非常高的复杂性,通常组织为了研发效能引入微服务,这对于运维管理来说是雪上加霜,它意味着频繁变更,但是从使用 K8s 的情况看,仅靠一条命令就完成了 6 个服务的部署,执行变更也是同样简单,流量治理这种原本与应用耦合的能力也解耦沉淀为平台层的通用能力,而从云原生定义的角度看,微服务和** Istio **都是其代表性技术的一部分,这一切是如何发生的呢?
本篇先考察应用部署的情况,了解了其中 K8s 的作用才能更好的理解 Istio 如何生效。一切神奇从对基础设施的抽象开始,在 K8s 的世界里,将服务器抽象为一组相关容器的集合,即 Pod ,它使用 pause 容器启用了一个共享的命名空间,使 Pod 内的容器共享一部分资源,例如通过 localhost 通信、共享存储卷等,类似在主机上运行进程的情况,这是一种编排思想,用来确保一组相关的资源(进程、数据等)能够配合工作, Pod 是实现这种思想的技术方案,当 Pod 能够被一种确定的语言描述,并能保证在运行过程中不被修改就使得构建一个 Pod 编排系统成为可能,编排系统可以通过创建和删除确定的 Pod 实现变更而不用考虑被随意修改后的不一致情况,这是云原生代表性技术不可变基础设施的实现。同时容器是一种轻量的隔离技术(容器是受限的进程),能够支持快速调度,同时不会消耗过多资源用于隔离本身,例如使用虚拟机隔离将消耗更多的资源,要注意,容器轻量但不是没有成本,比如他的网络通信会变得更加复杂从而影响通信性能,它也是云原生的代表性技术之一,另外一个需要注意的点是, Pod 设计符合容器设计模式中的“单主机多容器应用模式”(有兴趣可以阅读译|Design patterns for container-based distributed systems)。
如果在传统环境中部署示例应用,它们是一些无状态服务,不使用容器技术时,理想的情况是每个应用一个主机,这个主机中可能还包含一些辅助性的进程,例如完成主机初始化的短时进程、收集监控指标的常驻进程等,这样能够隔离故障域,在需要扩展某个应用时只需要在对应的应用集群中增加一些这样的主机,应用集群前面还要存在一个负载均衡用来分发流量,这种简单的模型为自动化奠定了良好的基础,前面提到在 K8s 中将主机抽象为了 Pod ,在我们执行一条命令完成示例应用部署时,实际上就是把期望的最终状态告诉 K8s ,然后由它不停的将实际状态调整为期望状态,稍微深入这个过程可以发现,首先提交者只是告知了最终状态,而不是在使用一条条命令指挥机器先干什么后干什么,其次系统会自动维护实际状态向期望状态迁移,这里的提交期望状态是另一个云原生代表技术**声明式 API **的实现,而它维护实际状态的方法是由相关的控制器不断运行的控制循环,这些都在指出 K8s 的本质:容器化应用的编排系统,微服务显然更需要这种平台。
以上介绍更多的偏向技术特性,稍微进入实现细节的话,示例应用中 reviews 这样的无状态服务在 K8s 中被抽象为 Deployment ,显然将由 Deployment 控制器来负责控制它的实际状态,但是应用在整个生命周期中的管理不仅仅是启动和杀死进程这样简单,必须要考虑到高可用,比如广泛采用的横向扩展和和滚动更新(新版本在线上逐步替换旧版本),因此 Deployment 是一个更高层的抽象,作为其他低层对象的属主来解构应用生命周期中的管理过程,参考下面的关系图:
虚线框内,从横向扩展的角度看,一个服务由多个实例构成,因此使用 ReplicaSet 维护 Pod 数量,从滚动升级的角度看,当 Pod 配置发生变更时,需要以此消彼长的方式更迭,就由 Deployment 在不同的 ReplicaSet 之间自动完成实例数量的调整,最终将 Pod 全部变更到新的配置,这种行为是在变更配置后自动发生的,当然也可以暂停滚动更新,在多次变更配置后启用,以减少滚动的次数,如果想在服务变更的过程中进行更加精细的控制,那么完全可以用两个不同的 Deployment 来描述不同的版本,然后手动或借助外部系统定义更新过程,这符合 K8s 官方文档介绍的金丝雀部署( Canary Deployments )实践,示例应用中的 reviews 服务就是这种部署方式,这里不过多分析 K8s 无状态应用解构的理由,我也没找到官方发布的设计文档,至少这些受 Deployment 管理的对象很好的拆分了关注点, ReplicaSet 也可以作为历史变更的保存媒介便于实现回滚。
继续深入一点, ReplicaSet 如何找到其所属的 Pod 并加以控制呢,答案是标签和选择器, 一方面标签具有非常强大的描述能力,另一方面选择器能够自动捕捉符合条件的对象,做到自动发现,更加适合动态的复杂环境,这也为手动介入提供了便利,列举一个场景,某个受管于 ReplicaSet 的 Pod A 出现问题后可以通过修改 Pod A 的标签进行隔离,这样 ReplicaSet 会创建新的 Pod 以维持我们定义的期望状态,而 Pod A 仍然处于运行状态,有利于继续排查问题。
K8s 用于实现自动化的资源和控制器方案也是它易扩展设计的一部分,是 K8s 扩展方式之一,用户可以使用它提供的 CRD API 免编码实现用户资源的定义和存取,通过自定义控制器实现用户资源的声明式 API 支持。
当然,要编排现实中的 IT 系统,必然还有大量的抽象,例如有状态应用、应用配置、存储、安全等,还需要考虑优先级、亲和性,这些就要在深入、系统学习的时候去了解和掌握,前面的内容至少使得 K8s 不再那么神秘。要注意的一点是, 虽然在无状态应用的场景中将 Pod 比作主机,但这个比喻在其他场景中并不总是成立,比如收集节点日志的守护进程也是以 Pod 形式存在,需要记住 Pod 的本质,即一组共享资源的容器、调度的最小单位。如果需要深入,建议两个途径:
- 极客时间课程《深入剖析 Kubernetes 》;
- K8s 官方文档 Kubernetes 文档;
本节的最后,挑选示例应用中 reviews 服务的声明文件来直观感受这种定义能力:
apiVersion: v1 # API 版本
kind: Service # 资源类型, Service 抽象了负载均衡
metadata: # 对象元数据
name: reviews # 对象名称,在命名空间内唯一,默认 default 命名空间
labels: # 标签,可用描述和选择
app: reviews
service: reviews
spec: # 定义该资源的行为
ports: # 服务暴露的端口
- port: 9080
name: http
selector: # 在当前命名空间筛选具备指定标签的 Pod 作为其服务实例
app: reviews
---
apiVersion: v1
kind: ServiceAccount # ServiceAccount 抽象了授权信息,详见:参考 1
metadata:
name: bookinfo-reviews
labels:
account: reviews
---
apiVersion: apps/v1
kind: Deployment # Deployment 抽象了无状态应用,是一个高级别对象,通过 ReplicaSet 管理 Pod
metadata:
name: reviews-v1
labels:
app: reviews
version: v1
spec:
replicas: 1 # 预期 Pod 的数量
selector: # 通过标签选择 ReplicaSet 所管理的 Pod 集合,与 Pod 标签匹配
matchLabels:
app: reviews # 标识 reviews 服务
version: v1 # 标识 reviews 服务的 v1 版本
template: # Pod 模板,定义了一个 Pod
metadata:
labels:
app: reviews
version: v1
spec:
serviceAccountName: bookinfo-reviews # 详见:参考 1
containers: # 容器列表
- name: reviews # 容器名称
image: docker.io/istio/examples-bookinfo-reviews-v1:1.18.0 # 容器镜像
imagePullPolicy: IfNotPresent # 镜像拉取策略
env: # 传递到容器的环境变量
- name: LOG_DIR
value: "/tmp/logs"
ports: # 容器暴露的端口
- containerPort: 9080
volumeMounts: # 容器挂载的存储卷
- name: tmp
mountPath: /tmp
- name: wlp-output
mountPath: /opt/ibm/wlp/output
volumes: # 存储卷定义
- name: wlp-output
emptyDir: {} # 为容器分配主机上的一个临时目录
- name: tmp
emptyDir: {}
---
apiVersion: apps/v1
kind: Deployment # 与前面的 Deployment 一致,只是存在版本差异,详见:参考 2
metadata:
name: reviews-v2
labels:
app: reviews
version: v2
spec:
replicas: 1
selector:
matchLabels:
app: reviews
version: v2
template:
metadata:
labels:
app: reviews
version: v2
spec:
serviceAccountName: bookinfo-reviews
containers:
- name: reviews
image: docker.io/istio/examples-bookinfo-reviews-v2:1.18.0
imagePullPolicy: IfNotPresent
env:
- name: LOG_DIR
value: "/tmp/logs"
ports:
- containerPort: 9080
volumeMounts:
- name: tmp
mountPath: /tmp
- name: wlp-output
mountPath: /opt/ibm/wlp/output
volumes:
- name: wlp-output
emptyDir: {}
- name: tmp
emptyDir: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: reviews-v3
labels:
app: reviews
version: v3
spec:
replicas: 1
selector:
matchLabels:
app: reviews
version: v3
template:
metadata:
labels:
app: reviews
version: v3
spec:
serviceAccountName: bookinfo-reviews
containers:
- name: reviews
image: docker.io/istio/examples-bookinfo-reviews-v3:1.18.0
imagePullPolicy: IfNotPresent
env:
- name: LOG_DIR
value: "/tmp/logs"
ports:
- containerPort: 9080
volumeMounts:
- name: tmp
mountPath: /tmp
- name: wlp-output
mountPath: /opt/ibm/wlp/output
volumes:
- name: wlp-output
emptyDir: {}
- name: tmp
emptyDir: {}
---
参考:
- ServiceAccount 用于声明 Pod 内进程访问 K8s APIServer 的凭据,在 1.24 版本之前, Secret 控制器会在监听到 ServiceAccount 创建事件后自动创建无限期的 Secret 并挂载至 Pod ,1.24 之后出于安全考虑会由 Kubelet 自动申请并挂载有限期的凭据并定时刷新,详情参考博客ServiceAccount在k8s1.24版本中的变化;
- Deployment 的标签标识了相同应用的三个不同版本,而 Service 使用应用标签筛选,最终三个不同的版本都会作为 Service 的实例,以负载均衡轮询的方式提供服务;
流量路径
上一节中介绍了确保进程运行的基本原理,这节主要来说明流量路径的问题,在传统的微服务中,不同服务之间通过服务名互相调用,这并不是一件稀奇的事情,因为在弹性系统中 IP 地址属于动态环境的一部分,将随着进程的调度频繁变更,名字服务将频繁变动的 IP 转换为固定的名称, K8s 中也是通过名字找到服务,不过这个过程相比类似 SpringCloud 中的注册、查询过程来说更加无感,是通过服务的全限定域名找到访问入口的, K8s 中通过运行 CoreDNS 来支持这个特性,这里还有一个关键的抽象: Service ,也就是负载均衡器,它是支持弹性的关键,否则调用方必须要知道被调用方的所有端点然后在调用方应用中作负载均衡, SprinCloud 就是这样干的,简单梳理一下 K8s 原生的方案:调用方通过被调用方 Service 的域名知道访问入口,由 Service 完成负载均衡,这是一个清晰且自动化的过程,但它对微服务间流量的控制能力实在有限,不能支持限流-熔断-降级,因此 Istio 几乎作为必选组件出现在云原生方案中,是云原生的典型技术。
Istio 通过外部进程劫持流量,以透明的方式完成流量控制,它的架构也许比较简单,不过要描述每个请求的路径、如何完成控制、与 K8s 的耦合点是什么等这些问题似乎并不太容易,所以下面会从 K8s 原生和 Istio 两个维度深入细节,来作一些更有价值的探查,互联网上似乎很少有这部分内容。
写到这里发现我的 K8s 实验环境有些问题, Worker 节点重启后 Pod 恢复缓慢, 一些容器相关进程 CPU 占用非常高, Kubelet 也是,常常导致节点不可用,打算抽时间仔细排查下问题再继续