有状态or无状态应用

对应用“有状态”和“无状态”的清晰界定,直接决定了它在Kubernetes中的部署方式、资源类型和运维复杂度。


一、核心定义

1. 无状态应用

定义:应用实例不负责保存每次请求所需的上下文或数据状态。任何一个请求都可以被任何一个实例处理,且处理结果完全一致。

关键特征

  • 请求自包含:每个请求包含了处理它所需的所有信息(如认证Token、Session ID、操作数据等)。
  • 实例可替代:任何一个实例都是完全相同、可以随时被创建或销毁的。销毁一个实例不会丢失任何数据。
  • 无本地持久化:实例的本地磁盘不被用于保存需要持久化的数据。即使有临时数据,实例销毁后也无需关心。
  • 水平扩展容易:因为实例完全相同,所以直接增加实例数量就能实现扩展,非常简单。

典型例子

  • Web前端服务器:如Nginx, Apache。
  • API网关:如Kong, Tyk。
  • JWT令牌验证服务
  • 无状态计算服务:如图片转换、数据格式转换等。输入和输出都在请求中完成。

一个生动的比喻快餐店的收银员。 任何一个收银员都可以为你服务,你点餐(请求),他处理,完成后交易结束。他不需要记住你上次点了什么(状态),你下次来可以去任何一个窗口。

2. 有状态应用

定义:应用实例需要保存和维护特定的状态数据。后续请求的处理依赖于之前请求保存的状态,或者会改变这个状态。

关键特征

  • 状态依赖性:请求的处理结果依赖于该实例上保存的特定状态(如用户会话、数据库中的记录、缓存数据等)。
  • 实例唯一性:每个实例都是独特的,有唯一的身份标识(如ID、主机名)。不能随意替换。
  • 需要持久化存储:实例的状态必须被保存在持久化存储中,并且即使实例重启、迁移或重建,这个存储也必须能被重新挂载和访问。
  • 水平扩展复杂:扩展时需要谨慎处理数据分片、副本同步、身份识别等问题。

典型例子

  • 数据库:MySQL, PostgreSQL, MongoDB, Redis。
  • 消息队列:Kafka, RabbitMQ。
  • 有状态中间件:如Etcd, Zookeeper。
  • 用户会话服务器:将用户Session保存在本地内存或磁盘的应用。

一个生动的比喻银行的客户经理。 你有一个指定的客户经理(特定实例),他了解你的所有财务历史和需求(状态)。如果你换了一个新经理,他需要花时间从头了解你的情况,而且可能无法立即获得你所有的历史文件(数据)。


二、在Kubernetes中的关键差异

这个界定在K8s中至关重要,因为它决定了你使用哪种工作负载资源。

特性无状态应用有状态应用
核心K8s资源DeploymentStatefulSet
Pod身份完全可互换,无唯一标识。名字是随机的(如 app-7c8b5f6d9-abcde)。有稳定、唯一的标识符,按顺序生成(如 mysql-0, mysql-1, mysql-2)。
启动/终止顺序并行,无顺序。有序部署(从0到N-1),有序扩缩容(从N-1到0),有序滚动更新
网络标识不稳定的Pod IP。通过Service负载均衡访问。稳定的网络标识。每个Pod会有一个稳定的DNS记录:<pod-name>.<svc-name>.<namespace>.svc.cluster.local
存储使用PersistentVolumeClaim模板,所有Pod共享同一个PVC或各自使用独立的、无关联的PVC。使用稳定的、专用的存储。每个Pod根据它的身份标识,挂载一个独立的PVC(如 mysql-0 -> pvc-mysql-0)。
数据持久性Pod被删除,其关联的PVC通常也会被删除(取决于回收策略)。Pod即使被调度到其他节点,也能通过稳定标识重新挂载到属于它的那块持久化数据。
典型场景Web服务器、微服务、API数据库、消息队列、集群化应用(如Zookeeper)

三、一个常见的误区:“看似无状态,实则有状态”

有些应用初看像无状态,但深究起来其实是有状态的。

  • 误区:一个将用户Session保存在本地内存的Web应用。
    • 看似:它是一个Web服务,可以通过Deployment部署多个副本。
    • 实则:如果用户第一次请求被pod-a处理,Session保存在了pod-a的内存中。下次请求如果被负载均衡到pod-bpod-b无法获取到该用户的Session,导致用户需要重新登录。
    • 解决方案
      1. 改造为无状态:将Session数据外移到集中式的Redis或数据库中。
      2. 承认其有状态:使用StatefulSet,并配合Session亲和性,确保同一用户的请求总是被发到同一个Pod实例上。

总结

如何界定一个应用是有状态还是无状态?

问自己这几个问题:

  1. 这个应用的实例能被随意杀死并立即创建一个新的替代吗? 替代者能无缝接管所有工作吗?
    • -> 无状态
    • 不能 -> 有状态
  2. 应用的多个实例是完全相同的吗? 增加一个实例需要复制数据吗?
    • 是,不需要 -> 无状态
    • 否,需要 -> 有状态
  3. 处理请求是否需要依赖实例本地(内存/磁盘)的、非临时性的数据?
    • -> 无状态
    • -> 有状态

理解这个界定,是正确设计和部署云原生应用的基石。在K8s中,对于无状态应用,请首选 Deployment;对于有状态应用,请务必使用 StatefulSet