1. 概述
2. 开启APF
APF测试 开启APF,需要在apiserver配置
提案。
API 优先级和公平性(1.15以上,alpha版本), 以更细粒度(byUser,byNamespace)对请求进行分类和隔离。 支持突发流量,通过使用公平排队技术从队列中分发请求从而避免饥饿。
APF限流通过两种资源,PriorityLevelConfigurations
定义隔离类型和可处理的并发预算量,还可以调整排队行为。 FlowSchemas
用于对每个入站请求进行分类,并与一个 PriorityLevelConfigurations
相匹配。
可对用户或用户组或全局进行某些资源某些请求的限制,如限制default namespace写services put/patch请求。
优点
考虑情况较全面,支持优先级,白名单等 可支持server/namespace/user/resource等细粒度级别的限流
缺点
配置复杂,不直观,需要对APF原理深入了解 功能较新,缺少生产环境验证
整体设计:
APF 的实现依赖两个非常重要的资源 FlowSchema, PriorityLevelConfiguration APF 对请求进行更细粒度的分类,每一个请求分类对应一个 FlowSchema (FS) FS 内的请求又会根据 distinguisher 进一步划分为不同的 Flow FS 会设置一个优先级 (Priority Level, PL),不同优先级的并发资源是隔离的。所以不同优先级的资源不会相互排挤。特定优先级的请求可以被高优处理。 一个 PL 可以对应多个 FS,PL 中维护了一个 QueueSet,用于缓存不能及时处理的请求,请求不会因为超出 PL 的并发限制而被丢弃。 FS 中的每个 Flow 通过 shuffle sharding 算法从 QueueSet 选取特定的 queues 缓存请求。 每次从 QueueSet 中取请求执行时,会先应用 fair queuing 算法从 QueueSet 中选中一个 queue,然后从这个 queue 中取出 oldest 请求执行。所以即使是同一个 PL 内的请求,也不会出现一个 Flow 内的请求一直占用资源的不公平现象。
注意: 属于 “长时间运行” 类型的某些请求(例如远程命令执行或日志拖尾)不受 API 优先级和公平性过滤器的约束。 如果未启用 APF 特性,即便设置 --max-requests-inflight
标志,该类请求也不受约束。 APF 适用于 watch 请求。当 APF 被禁用时,watch 请求不受 --max-requests-inflight
限制。
3. FlowSchema
3.1 对请求进行分类
用户可以通过创建 FlowSchema 资源对象自定义分类方式。
FS 代表一个请求分类,包含多条匹配规则,如果某个请求能匹配其中任意一条规则就认为这个请求属于这个 FS (只匹配第一个匹配的 FS)。
type PolicyRulesWithSubjects struct {
Subjects []Subject
ResourceRules []ResourcePolicyRule
NonResourceRules []NonResourcePolicyRule
}
type Subject struct {
Kind SubjectKind `json:"kind" protobuf:"bytes,1,opt,name=kind"`
User *UserSubject `json:"user,omitempty" protobuf:"bytes,2,opt,name=user"`
Group *GroupSubject `json:"group,omitempty" protobuf:"bytes,3,opt,name=group"`
ServiceAccount *ServiceAccountSubject `json:"serviceAccount,omitempty" protobuf:"bytes,4,opt,name=serviceAccount"`
}
type ResourcePolicyRule struct {
Verbs []string `json:"verbs" protobuf:"bytes,1,rep,name=verbs"`
APIGroups []string `json:"apiGroups" protobuf:"bytes,2,rep,name=apiGroups"`
Resources []string `json:"resources" protobuf:"bytes,3,rep,name=resources"`
ClusterScope bool `json:"clusterScope,omitempty" protobuf:"varint,4,opt,name=clusterScope"`
Namespaces []string `json:"namespaces" protobuf:"bytes,5,rep,name=namespaces"`
}
type NonResourcePolicyRule struct {
Verbs []string `json:"verbs" protobuf:"bytes,1,rep,name=verbs"`
NonResourceURLs []string `json:"nonResourceURLs" protobuf:"bytes,6,rep,name=nonResourceURLs"`
}
通过 FS,可以根据请求的主体 (User, Group, ServiceAccout)、动作 (Get, List, Create, Delete …)、资源类型 (pod, deployment …)、namespace、url 对请求进行分类。
FS 内的请求进一步划分 Flow 有两种方式对请求进行 Flow 划分:
distinguisher = ByUser, 根据请求的 User 划分不同 Flow;可以让来自不同用户的请求平等使用 PL 内的资源。 distinguisher = ByNamespace, 根据请求的 namespace 划分不同的 Flow;可以让来自不同 namespace 的请求平等使用 PL 内的资源。 distinguisher = nil,表示不划分 3.2 Priority Level
如果 api-sever 启动了 APF,它的总并发数为 --max-requests-inflight
和 --max-mutating-requests-inflight
两个配置值之和。这些并发数被分配给各个 PL,分配方式是根据 PriorityLevelConfiguration.Spec.Limited.AssuredConcurrencyShares 的数值按比例分配。 PL 的 AssuredConcurrencyShare 越大,分配到的并发份额越大 。
每个 PL 都对应维护了一个 QueueSet,其中包含多个 queue ,当 PL 达到并发限制时,收到的请求会被缓存在 QueueSet 中,不会丢弃,除非 queue 也达到了容量限制。
当入站请求的数量大于分配的 PriorityLevelConfiguration 中允许的并发级别时, type
字段将确定对额外请求的处理方式。 Reject
类型,表示多余的流量将立即被 HTTP 429(请求过多)错误所拒绝。 Queue
类型,表示对超过阈值的请求进行排队,将使用阈值分片和公平排队技术来平衡请求流之间的进度。
QueueSet 中 queue 数量由PriorityLevelConfiguration.Spec.Limited.LimitResponse.Queuing.Queues 指定;每个 queue 的长度由 PriorityLevelConfiguration.Spec.Limited.LimitResponse.Queuing.QueueLengthLimit 指定。
例如,默认配置包括针对领导者选举请求、内置控制器请求和 Pod 请求都单独设置优先级。 这表示即使异常的 Pod 向 API 服务器发送大量请求,也无法阻止领导者选举或内置控制器的操作执行成功。
优先级的并发限制会被定期调整,允许利用率较低的优先级将并发度临时借给利用率很高的优先级。 这些限制基于一个优先级可以借出多少个并发度以及可以借用多少个并发度的额定限制和界限, 所有这些均源自下述配置对象。
请求占用的席位
PL request global-default 所有其他的请求 leader-election 优先级用于内置控制器的领导选举的请求 (特别是来自 kube-system
名字空间中 system:kube-controller-manager
和 system:kube-scheduler
用户和服务账号,针对 endpoints
、configmaps
或 leases
的请求) node-high
优先级用于来自节点的健康状态更新。 system 优先级用于 system:nodes
组(即 kubelet)的与健康状态更新相关的请求; kubelets 必须能连上 API 服务器,以便工作负载能够调度到其上 workload-high 优先级用于内置控制器的其他请求 workload-low 优先级用于来自所有其他服务帐户的请求,通常包括来自 Pod 中运行的控制器的所有请求。
// sort into the order to be used for matching
sort.Sort(fsSeq)
// Supply missing mandatory FlowSchemas, in correct position
if !haveExemptFS {
// 放在第一位
fsSeq = append(apihelpers.FlowSchemaSequence{fcboot.MandatoryFlowSchemaExempt}, fsSeq...)
}
if !haveCatchAllFS {
// 放在最后一位
fsSeq = append(fsSeq, fcboot.MandatoryFlowSchemaCatchAll)
}
自定义PL和FS
apiVersion: flowcontrol.apiserver.k8s.io/v1alpha1
kind: PriorityLevelConfiguration
metadata:
name: leader-election
spec:
limited: #限制策略
assuredConcurrencyShares: 10
limitResponse: #如何处理被限制的请求
queuing: #类型为Queue时,列队的设置
handSize: 4 #队列
queueLengthLimit: 50 #队列长度
queues: 16 #队列数
type: Queue #Queue或者Reject,Reject直接返回429,Queue将请求加入队列
type: Limited #类型,Limited或Exempt, Exempt即不限制
4. 问题诊断
启用了 APF 的 API 服务器,它每个 HTTP 响应都有两个额外的 HTTP 头: X-Kubernetes-PF-FlowSchema-UID
和 X-Kubernetes-PF-PriorityLevel-UID
, 注意与请求匹配的 FlowSchema 和已分配的优先级。 如果请求用户没有查看这些对象的权限,则这些 HTTP 头中将不包含 API 对象的名称, 因此在调试时,你可以使用类似如下的命令:
5. 源码分析
5.1 请求流程
API Server 接收到请求后,先按照前面提到的方式,找到与之匹配的 FS,实现分类,并根据 FS 确定请求的所属的Flow 和 PL。
APF 利用 FS 的 name 和请求的 userName 或 namespace 计算一个 hashFlowID 标识 Flow。
func (d *Dealer) Deal(hashValue uint64, pick func(int)) {
// 15 is the largest possible value of handSize
var remainders [15]int
for i := 0; i < d.handSize; i++ {
hashValueNext := hashValue / uint64(d.deckSize-i)
remainders[i] = int(hashValue - uint64(d.deckSize-i)*hashValueNext)
hashValue = hashValueNext
}
// 防止重复:正反馈机制,大者更大
for i := 0; i < d.handSize; i++ {
card := remainders[i]
for j := i; j > 0; j-- {
if card >= remainders[j-1] {
// 不会出现 card > deckSize
// 因为 hashValue % uint64(d.deckSize-i) <= d.deckSize-i-1,而第 i 个 card 最多自增 i 次
card++
}
}
pick(card)
}
}
判断是否入队这个请求:如果队列已满且 PL 中正在执行的请求数达到 PL 的并发限制,就会拒绝这个请求,否则入队这个请求。
此处保证了不同 Flow 的请求不会挤掉其他 Flow 的请求。Flow 是按照用户或 namespace 划分的,它的实际意义就是来自不同用户或 namespace 的请求不会挤掉同优先级的其他用户或 namespace 的请求。
5.2 分发请求
为了保证同一个 PL 中缓存的不同 Flow 的请求被处理机会平等,每次分发请求时,都会先应用 fair queuing 算法从 PL 的 QueueSet 中选中一个 queue:
type queue struct {
requests []*request
// 如果队列中没有 request 且没有 request 在执行 (requestsExecuting = 0), virtualStart = queueSet.virtualTime
// 每分发一个 request, virtualStart = virtualStart + queueSet.estimatedServiceTime
// 每执行完一个 request, virtualStart = virtualStart - queueSet.estimatedServiceTime + actualServiceTime,用真实的执行时间,校准 virtualStart
// 计算第 J 个 request 的 virtualFinishTime = virtualStart + (J+1) * serviceTime
virtualStart float64
requestsExecuting int
index int
}
virtualStart 初始化是直接设置为 QueueSet 中维护的 virtualTime。而 QueueSet.virtualTime 是在这个 PL 初始化的时候赋值为 0。此后,如果 QueueSet 中的 queue 如有任何状态变化,都要执行更新,根据自身两次变更历经的 realTime 按比例增加:
func (qs *queueSet) syncTimeLocked() { realNow := qs.clock.Now() timeSinceLast := realNow.Sub(qs.lastRealTime).Seconds() qs.lastRealTime = realNow qs.virtualTime += timeSinceLast * qs.getVirtualTimeRatioLocked() }
其中,这个比例计算方式为:min(QueueSet 中正字执行的请求数, PL 的并发配额) / QueueSet 中活跃的 queue 数目。
virtualTime 实际对应于 bit-by-bit round-robin 算法中的 R(t),当前时间 round-robin 轮数。具体可以参考文后第4个链接。
选 queue 时,会估计每个 queue 中 oldest 请求的虚拟执行完毕时间,选择这个虚拟执行完毕时间最小的 queue
选中 queue 之后,从 queue 中取出 oldest 请求,设置执行标记。重复执行以上选 queue 给 oldest 请求设置执行标志,直到 PL 所有的 Queue 中都没有缓存的请求或达到 PL 的并发限制。
注:此处是尽可能多的分发 PL 中缓存的请求,有可能当前新加入的请求不会被分发。
5.3 请求阻塞监听执行
完成以上操作之后,该请求会进入阻塞监听状态,直到被分发。
5.4 请求执行
如果这个请求被唤醒,并收到了 decisionExecute 标记,便会开始执行。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)