本文最初以英文写成。
这个issue和controller以及handler有关,而这部分代码我并不熟悉,所以花了我好几天才搞明白crd server内部到底是怎么跑起来的。
CRD server(也许这个叫法不太准确?)其实是包在api-server里面的,代码里实际的名字叫apiextension-server。它负责处理CRD请求,以及事件到来时的各种event,这些event是由一个shared Informer发过来的。这里我想补充一点关于Informer的内容:它是一个专门用来做同步、消费队列并把事件分发到目的地的组件。
接下来就轮到我们的controller了——它会实时地从CRD-server拿到这些事件,触发自己预先写好的逻辑,并且很可能引起对应资源的状态变化。第二部分是handler。这个handler和我们在go标准库http里见到的是同一种东西,就是一个函数,同样接收responseWriter和request指针。发往CRD的请求会被分发到这个函数上,而每个controller通常都有自己对应的handler。
现在理解这个issue所需要的东西都齐了,我们先看看这个issue里到底发生了什么:当有人更新了CRD spec并立刻请求对应的endpoint时,server返回了一个过期的响应——它没有跟上最新的改动。也就是说问题出在新鲜度上。那是什么造成的呢?spec的变更应该会产生一个事件,被controller和informer消费掉,这样它们才能更新自己的状态。如果往代码深处看,会发现informer往它们发事件的顺序是不固定的——这是对的,而且是刻意为之,因为这样它就不会卡在某个很慢的消费者上。再往下翻handler和controller的代码,我发现discovery controller负责发布discovery document。它应该是很关键的一环,因为原始issue里描述的症状就是url对不上。但它的逻辑是没问题的。
于是我把注意力放到crdHandler上,它才是真正管理CRD REST storage的那个handler。然后我找到了根因:这个handler用crd.UID去一个可能已经过期的cache里查,而且完全没有检查cache是不是最新的。所以当Informer把事件同时发给discovery controller和crdHandler时,race就出现在这里。如果discovery controller比crdHandler快,那么请求到达的时候,crdHandler手里还拿着一份过期的cache,根本不知道spec已经变了。反过来的顺序才是正常、符合预期的路径。
问题找到了,这其实是最难的部分。修法就很清楚了。既然我们不能去改informer的行为,那样影响面太大,那能做的就是让crdHandler在spec变化时更新自己的store。所以我把用crd.UID查cache map的那段逻辑去掉了,改成用generation来判断spec有没有变过。
这里有个坑。generation好用,但还不够。如果expectedName(在crd status里)和别的冲突了,而这种情况server更新时是不会bump generation的,crdHandler就不会知道,可能会构建出错误的storage。所以我们还需要加一个函数来比较它们是否相等,用来判断要不要更新storage。