kubernetes PV控制器中存储对象的更新(storeObjectUpdate())存在竞态条件,

vkc1a9a2  于 4个月前  发布在  Kubernetes
关注(0)|答案(8)|浏览(62)

发生了什么?
PV控制器(在KCM)除了informer缓存之外,还有自己的缓存来存储自身的更新。然而,我们已经发现了在多个goroutine中调用storeObjectUpdate()时存在2个可能的竞态条件。
storeObjectUpdate()目前从setClaimProvisioner()syncUnboundClaim()调用,这两个都在不同的goroutine中运行。我预计未来会有更多的并发访问以提高吞吐量。
竞态条件:

  1. 内部更新覆盖外部删除。当一个对象从APIServer被删除时,控制器本身的更新请求可能仍在飞行中。更新返回后,它试图将对象插入到缓存中,覆盖来自informer的删除传播。结果是对象永远留在缓存中。随后导致例如PV保持Bound状态永远无法释放等问题。
  2. storeObjectUpdate()中的读-比较-写操作。当并发调用时,旧版本(由ResourceVersion定义)可能会覆盖新版本。导致15秒的缓存停滞(应在下一次同步时修复)。

我们期望发生什么?

没有竞态条件。内部缓存在并发访问时保持一致。

我们如何尽可能精确地最小化地重现它?

难以重现,这种情况发生的几率很小。
当我们尝试优化PV控制器的吞吐量时,我们观察到了第一种情况。

我们需要了解的其他信息吗?

为了解决竞态条件2,我建议:在storeObjectUpdate()周围添加锁
为了解决竞态条件1,我建议:明确区分更新和添加事件,不允许内部更新响应中的添加操作。

Kubernetes版本

$ kubectl version
# paste output here

云提供商

OS版本

# On Linux:
$ cat /etc/os-release
# paste output here
$ uname -a
# paste output here

# On Windows:
C:\> wmic os get Caption, Version, BuildNumber, OSArchitecture
# paste output here

安装工具

容器运行时(CRI)和版本(如适用)

相关插件(CNI,CSI等)和版本(如适用)

pepwfjgg

pepwfjgg2#

  1. 内部更新覆盖外部删除。当对象从APIServer被删除时,控制器本身可能仍在进行中。在更新返回后,它尝试将对象插入回缓存,覆盖来自informer的删除传播。结果是对象永远留在缓存中。这进而导致,例如,PV保持在Bound状态永远无法释放。
    @huww98 你是否在实际集群中观察到这种竞争条件?我认为这主要是理论上的。
    我可以想象一种新参数storeObjectUpdate(..., store cache.Store, obj interface{}, force bool),当对象在缓存中缺失时仅在force == true时存储该对象。这仅在初始同步或作为对informer事件的React使用。否则,如果对象缺失(即必须从informer中删除),则不会将其添加回缓存。当然,这只是在缓存之上的一个丑陋的hack。最好摆脱整个缓存。欢迎提出聪明的想法!

  2. 在storeObjectUpdate()中的读-比较-写操作。当并发调用时,旧版本(由ResourceVersion定义)可能会覆盖新版本。导致无限制的缓存停滞。
    这个问题应该通过每15秒进行一次周期性同步来修复。我知道我们不应该依赖它,15秒有点激进,但在这里有效。

mrfwxfqh

mrfwxfqh3#

你是否在真实的集群中观察到这种竞争条件?我认为这主要是理论上的。
是的,我在我们的生产集群中看到过这种情况,那里有一些补丁可以提高PV控制器的吞吐量。我们看到一个PV无缘无故地保持在Bound状态。在我们再次创建和删除引用的PVC之后,PV成功释放了。我们至少看到了两次这种情况。
我可以想象一种新参数storeObjectUpdate(..., store cache.Store, obj interface{}, force bool),当它在缓存中缺失时,只有在force == true的情况下才会存储对象。这只会在初始同步或对informer事件做出React时使用。否则,如果对象缺失(即必须从informer中删除),那么它就不会被添加回缓存。当然,这只是在缓存之上的一个丑陋的hack。最好还是摆脱整个缓存。欢迎提出好主意!
是的,这正是我在内部分支中尝试修复它的方式。
我们也可以在informer add事件处理器和init中使用Store.Add(),然后将storeObjectUpdate()更改为仅支持更新。
我尝试了另一种方法。以informer缓存为真相,抑制停滞事件。使用sync.Map来维护每个UID的最新代,然后忽略停滞对象的更新事件,或者在同步时不考虑停滞对象。至少在这种方法中,我们不能把已删除的对象误认为是存在的。但是我需要检查读取缓存时的生成情况。我们可以使用CompareAndSwap来更新生成Map,这似乎更轻量级和优雅。我们还可以在这个方法中充分利用informer索引器。
这个应该通过每隔15秒进行一次周期性同步来解决。我知道我们不应该依赖它,15秒有点激进,但在这里有效。
我同意,已经编辑了问题以反映这一点。

zkure5ic

zkure5ic4#

我尝试了另一种方法。将informer缓存视为真相,并抑制停滞事件。使用sync.Map来维护每个UID的最新版本,然后忽略停滞对象的更新事件,或者在同步时不考虑停滞对象。至少在这种方法中,我们不能把已删除的对象误认为是存在的。但在读取缓存时,我需要检查生成情况。我们可以使用CompareAndSwap来更新生成Map,这似乎更轻量级和优雅。我们还可以充分利用informer索引器。

metdata.generation 是否适用于PVs和PVCs?我看到它们都是空的。

d8tt03nd

d8tt03nd5#

metdata.generation 是否适用于PV和PVC?我看到它们都是空的。
目前不行,但我认为我们可以让它起作用。或者我们仍然可以使用资源版本,原则不应该改变。

1sbrub3j

1sbrub3j6#

generation 通常会在 .spec 改变时改变,而不是在 .status 改变时。PV 控制器一次性保存 pv.specpvc.specpv.statuspvc.status,如果我们能忽略 .status 的更新就更好了。我不确定这是否与代数有关。

fd3cxomn

fd3cxomn8#

/cc @jsafrane
related [#25881](https://github.com/kubernetes/kubernetes/pull/25881)

相关问题