别再死磕固定服务器了:用 Knative 把“来多少活干多少活”玩明白
作者:Echo_Wish
一个运维人经常会遇到这样的场景:系统平时没什么流量,到了某个时间点突然一窝蜂涌进来。你扩容吧,平时浪费资源;不扩容吧,高峰一来 CPU 飙到 100%,告警响个不停。
有没有一种办法,让系统有活就干、没活就歇,活多了自动加人,活少了自动减人?
有。
这就是今天要聊的 Knative + Kubernetes + Event-driven。
一、先说一个运维人最头疼的问题:服务器到底该准备多少?
假设你维护一个图片处理平台。
平时:
每秒 2 个请求
CPU:10%
内存:1GB
到了晚上:
每秒 500 个请求
CPU:95%
内存:7GB
最传统的办法是什么?
提前准备 10 台服务器。
问题来了:
白天怎么办?
10 台服务器
↓
真正干活的
↓
可能只有 1 台
剩下 9 台:
“我今天也没啥事,就是待着。”
这就是传统固定容量架构的尴尬。
于是很多团队开始搞 Kubernetes HPA:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: image-service
spec:
minReplicas: 2
maxReplicas: 20
targetCPUUtilizationPercentage: 70
看起来不错。
但这里其实有一个很大的问题:
CPU 并不一定代表业务压力。
比如消息队列里面积压了 10 万条消息:
Kafka
│
├── 100000 条消息
│
↓
Consumer
│
└── CPU 可能只有 20%
这时候你盯着 CPU:
“CPU 才 20%,不用扩容。”
结果业务已经开始排队。
所以真正应该关注的,有时候不是:
“服务器有多忙?”
而是:
“现在到底有多少活没干完?”
这就是 Event-driven 思路真正有价值的地方。
二、Knative 到底解决什么问题?
Knative 可以简单理解成:
给 Kubernetes 加了一套“按业务请求自动伸缩”的能力。
它最核心的几个组件可以粗略理解成:
┌──────────────┐
│ Kubernetes │
└──────┬───────┘
│
┌───────▼───────┐
│ Knative │
└───────┬───────┘
│
┌──────────────┼──────────────┐
│ │ │
Serving Eventing Autoscaling
│ │ │
服务部署 事件驱动 自动伸缩
其中:
Knative Serving 主要负责服务部署和弹性伸缩。
Knative Eventing 负责:
“谁产生了事件?这个事件应该送给谁?”
这两个东西结合起来,就能形成一个比较完整的事件驱动平台。
三、先搞明白一个核心思想:别让服务一直等活
传统服务通常是:
启动 Pod
↓
一直运行
↓
等待请求
↓
处理请求
↓
继续等待
这其实挺浪费。
Knative 更希望变成:
没有请求
↓
0 个实例
↓
来了请求
↓
自动启动
↓
处理请求
↓
流量下降
↓
自动缩容
也就是说:
没有活的时候,可以不养人。
这就是 Knative 最让人上头的地方。
四、先玩 Knative Serving
假设我们有一个订单服务:
order-service
最简单可以定义一个 Knative Service。
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: order-service
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/minScale: "0"
autoscaling.knative.dev/maxScale: "20"
spec:
containers:
- image: myregistry/order-service:v1
ports:
- containerPort: 8080
这里最值得关注的是:
minScale: "0"
maxScale: "20"
意思很简单:
最少:0
最多:20
也就是说:
没请求
↓
0 Pod
请求来了
↓
1 Pod
请求继续增加
↓
2 Pod
继续增加
↓
5 Pod
再增加
↓
10 Pod
当然,实际扩缩容还受到并发数、启动时间、资源等因素影响。
但思想就是这么简单。
五、真正有意思的地方:事件驱动
假设我们现在做一个电商系统。
用户下单以后,并不是所有事情都应该在一个 HTTP 请求里面完成。
比如:
用户下单
│
├── 创建订单
│
├── 扣库存
│
├── 生成发票
│
├── 发送短信
│
├── 推荐系统
│
└── 数据分析
如果全部同步执行:
POST /order
│
├── 订单
├── 库存
├── 发票
├── 短信
├── 推荐
└── 分析
用户可能等半天。
而事件驱动的思路是:
创建订单
│
▼
OrderCreated
│
┌────────┼────────┐
▼ ▼ ▼
库存服务 短信服务 分析服务
订单创建成功以后:
“我告诉大家订单创建了。”
至于:
“你们各自怎么处理,那是你们自己的事情。”
这就是事件驱动架构最核心的思想之一。
六、Knative Eventing 就是干这个的
Knative Eventing 可以把各种事件连接起来。
例如:
Kafka
│
▼
Event Source
│
▼
Broker
│
▼
Trigger
│
▼
Knative Service
你可以把它理解成一个“事件中转站”。
比如:
Kafka
│
│ OrderCreated
▼
Broker
│
├──── Trigger ────> inventory-service
│
├──── Trigger ────> sms-service
│
└──── Trigger ────> analytics-service
这时候服务之间就不需要互相强依赖。
七、Broker:事件世界里的“快递分拣中心”
Knative Eventing 里面一个非常重要的概念就是 Broker。
例如:
apiVersion: eventing.knative.dev/v1
kind: Broker
metadata:
name: default
你可以把 Broker 理解成:
事件快递站。
生产者把事件扔进来:
Producer
│
▼
Broker
然后消费者订阅:
Broker
│
├── 库存服务
├── 订单服务
├── 消息服务
└── 数据分析服务
这样做有一个非常明显的好处:
生产者不需要知道消费者是谁。
这点非常重要。
因为系统一旦做大,最怕的就是:
A 调 B
B 调 C
C 调 D
D 调 E
E 又调 A
最后整个系统变成:
“谁都不敢改。”
事件驱动的一个重要价值,就是尽可能降低这种耦合。
八、Trigger:告诉系统“什么事件给谁”
比如:
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
name: order-trigger
spec:
broker: default
filter:
attributes:
type: com.example.order.created
subscriber:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: order-service
这段配置其实很好理解。
它说的是:
Broker 里面如果出现
order.created事件,就把它交给order-service。
于是:
OrderCreated
│
▼
Broker
│
▼
Trigger
│
▼
order-service
这就开始有点“事件总线”的味道了。
九、最爽的一点:事件来了,服务才启动
假设凌晨 3 点。
系统:
订单服务:0 Pod
库存服务:0 Pod
短信服务:0 Pod
分析服务:0 Pod
这时候突然来了一个事件:
OrderCreated
Knative 发现:
“有人来活了。”
于是:
0 Pod
↓
启动
↓
1 Pod
↓
处理事件
如果突然来了 10000 个事件:
10000 Events
│
▼
Broker
│
▼
Autoscaler
│
├── Pod 1
├── Pod 2
├── Pod 3
├── ...
└── Pod N
这才是真正意义上的:
业务驱动资源,而不是人为猜测资源。
十、但这里有一个坑:冷启动
很多人看到 Knative 的:
0 → 1
非常兴奋。
但运维人不能只看到“省钱”。
因为:
0 Pod
↓
创建 Pod
↓
拉镜像
↓
启动容器
↓
加载应用
↓
应用 Ready
↓
开始处理请求
这一套是需要时间的。
如果你的应用启动需要:
8 秒
那么用户可能就会感受到:
“怎么第一次请求这么慢?”
这就是 Knative 的经典问题:
Cold Start,冷启动。
所以我的观点一直比较明确:
Serverless 不是免费的午餐,省掉的是闲置资源,不是复杂度。
十一、解决冷启动,不要一上来就想着“全部 0 副本”
比如核心支付服务:
autoscaling.knative.dev/minScale: "1"
保证至少一个实例。
而一些非核心服务:
autoscaling.knative.dev/minScale: "0"
允许缩到 0。
于是:
核心服务
↓
至少 1 个
低频服务
↓
允许 0 个
这其实比:
“所有服务全部 Scale to Zero!”
靠谱得多。
架构设计从来不是追求一个参数走天下。
十二、再来看一个完整架构
我们假设现在做一个订单事件平台:
用户
│
▼
┌─────────────┐
│ API Gateway │
└──────┬──────┘
│
▼
┌─────────────┐
│ Order API │
└──────┬──────┘
│
│ OrderCreated
▼
┌─────────────┐
│ Broker │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
Inventory SMS Service Analytics
Service
│ │ │
▼ ▼ ▼
DB SMS Data Lake
而每一个服务:
Knative Service
│
▼
Autoscaler
│
┌─────┼─────┐
▼ ▼ ▼
Pod Pod Pod
最终形成:
Kubernetes 提供基础设施,Knative 提供弹性,Eventing 提供事件连接。
这才是比较完整的事件驱动平台思路。
十三、什么时候特别适合 Knative?
我个人认为,下面几类业务非常适合。
第一类:流量波动特别大
比如:
平时:10 QPS
活动:
10000 QPS
这种场景固定扩容特别浪费。
第二类:异步任务
例如:
图片处理
视频转码
PDF解析
AI推理
报表生成
数据清洗
这些业务通常:
有任务 → 干活
没任务 → 休息
跟 Knative 的思想非常匹配。
第三类:事件驱动系统
例如:
订单创建
库存变化
支付成功
物流更新
用户注册
设备上线
这些天然就是 Event-driven。
十四、但不是所有系统都适合 Knative
这一点我特别想强调。
不要因为:
“Knative 很先进。”
就把所有服务全部迁过去。
比如一个:
数据库连接池特别重
启动需要 30 秒
本地缓存几十 GB
的服务。
你让它:
0 → 1 → 0 → 1 → 0
可能直接把自己玩崩。
还有一些:
长连接
WebSocket
超低延迟
强状态服务
也需要谨慎设计。
所以 Knative 最适合的是:
无状态、可水平扩展、生命周期短、流量波动明显、事件驱动明显的服务。
十五、运维人的思维也应该变一下
以前我们做运维,经常问:
“这台服务器 CPU 多少?”
现在应该逐渐开始问:
“业务现在有多少事件?”
以前:
CPU > 80%
↓
扩容
现在:
事件积压
↓
扩容
以前:
服务器
↓
24小时运行
现在:
事件
↓
服务
↓
实例
↓
处理完成
↓
缩容
这其实是一种非常明显的思维变化。
十六、最后聊聊我的一个观点
我一直觉得,Knative 真正有价值的地方,并不是:
“它可以让 Pod 从 10 个变成 5 个。”
这只是表面。
真正重要的是:
它让基础设施开始跟着业务节奏走。
以前是:
先买资源
↓
等业务
现在逐渐变成:
业务来了
↓
需要资源
↓
自动创建
↓
业务结束
↓
自动释放
这才是云原生真正有意思的地方。
但也别把 Serverless 想得太美。
冷启动、事件丢失、消息重复、幂等、可观测性、链路追踪、事件顺序、失败重试、死信队列……
这些问题一个都不会因为用了 Knative 就自动消失。
相反,系统越弹性,运维越应该关注:
事件有没有丢?
事件处理了几次?
为什么重试?
消费者为什么变慢?
为什么突然扩容?
为什么一直 Scale to Zero?
为什么冷启动这么慢?
所以我更愿意把 Knative 看成一种架构思想的升级:
从“我准备多少服务器”,变成“我的业务现在需要多少计算能力”。
这句话,可能才是 Knative 最值得我们琢磨的地方。
写在最后
如果让我用一句最接地气的话总结 Knative:
以前是养一群人等着干活,现在是来了多少活,就临时叫多少人过来干。
Kubernetes 解决的是:
“这些人住在哪里、怎么管理?”
Knative 解决的是:
“什么时候叫人来、什么时候让人回家?”
Eventing 解决的是:
“活来了以后,到底应该交给谁?”
三者组合起来:
Kubernetes
+
Knative Serving
+
Knative Eventing
↓
弹性事件驱动平台
而这可能也是未来云原生平台越来越重要的一条路线:
基础设施不应该一直等着业务,而应该随着业务一起“呼吸”。
这,才是我理解的 Event-driven。