发生了什么?
PV控制器(在KCM)除了informer缓存之外,还有自己的缓存来存储自身的更新。然而,我们已经发现了在多个goroutine中调用storeObjectUpdate()
时存在2个可能的竞态条件。storeObjectUpdate()
目前从setClaimProvisioner()
和syncUnboundClaim()
调用,这两个都在不同的goroutine中运行。我预计未来会有更多的并发访问以提高吞吐量。
竞态条件:
- 内部更新覆盖外部删除。当一个对象从APIServer被删除时,控制器本身的更新请求可能仍在飞行中。更新返回后,它试图将对象插入到缓存中,覆盖来自informer的删除传播。结果是对象永远留在缓存中。随后导致例如PV保持Bound状态永远无法释放等问题。
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
8条答案
按热度按时间rbl8hiat1#
/triage accepted
pepwfjgg2#
内部更新覆盖外部删除。当对象从APIServer被删除时,控制器本身可能仍在进行中。在更新返回后,它尝试将对象插入回缓存,覆盖来自informer的删除传播。结果是对象永远留在缓存中。这进而导致,例如,PV保持在Bound状态永远无法释放。
@huww98 你是否在实际集群中观察到这种竞争条件?我认为这主要是理论上的。
我可以想象一种新参数
storeObjectUpdate(..., store cache.Store, obj interface{}, force bool)
,当对象在缓存中缺失时仅在force == true
时存储该对象。这仅在初始同步或作为对informer事件的React使用。否则,如果对象缺失(即必须从informer中删除),则不会将其添加回缓存。当然,这只是在缓存之上的一个丑陋的hack。最好摆脱整个缓存。欢迎提出聪明的想法!在storeObjectUpdate()中的读-比较-写操作。当并发调用时,旧版本(由ResourceVersion定义)可能会覆盖新版本。导致无限制的缓存停滞。
这个问题应该通过每15秒进行一次周期性同步来修复。我知道我们不应该依赖它,15秒有点激进,但在这里有效。
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秒有点激进,但在这里有效。
我同意,已经编辑了问题以反映这一点。
zkure5ic4#
我尝试了另一种方法。将informer缓存视为真相,并抑制停滞事件。使用sync.Map来维护每个UID的最新版本,然后忽略停滞对象的更新事件,或者在同步时不考虑停滞对象。至少在这种方法中,我们不能把已删除的对象误认为是存在的。但在读取缓存时,我需要检查生成情况。我们可以使用CompareAndSwap来更新生成Map,这似乎更轻量级和优雅。我们还可以充分利用informer索引器。
metdata.generation
是否适用于PVs和PVCs?我看到它们都是空的。d8tt03nd5#
metdata.generation
是否适用于PV和PVC?我看到它们都是空的。目前不行,但我认为我们可以让它起作用。或者我们仍然可以使用资源版本,原则不应该改变。
1sbrub3j6#
generation
通常会在.spec
改变时改变,而不是在.status
改变时。PV 控制器一次性保存pv.spec
、pvc.spec
、pv.status
和pvc.status
,如果我们能忽略.status
的更新就更好了。我不确定这是否与代数有关。slhcrj9b7#
/sig storage
fd3cxomn8#