[{"content":" 本文最初以英文写成。\n这个issue和controller以及handler有关，而这部分代码我并不熟悉，所以花了我好几天才搞明白crd server内部到底是怎么跑起来的。\nCRD server（也许这个叫法不太准确？）其实是包在api-server里面的，代码里实际的名字叫apiextension-server。它负责处理CRD请求，以及事件到来时的各种event，这些event是由一个shared Informer发过来的。这里我想补充一点关于Informer的内容：它是一个专门用来做同步、消费队列并把事件分发到目的地的组件。\n接下来就轮到我们的controller了——它会实时地从CRD-server拿到这些事件，触发自己预先写好的逻辑，并且很可能引起对应资源的状态变化。第二部分是handler。这个handler和我们在go标准库http里见到的是同一种东西，就是一个函数，同样接收responseWriter和request指针。发往CRD的请求会被分发到这个函数上，而每个controller通常都有自己对应的handler。\n现在理解这个issue所需要的东西都齐了，我们先看看这个issue里到底发生了什么：当有人更新了CRD spec并立刻请求对应的endpoint时，server返回了一个过期的响应——它没有跟上最新的改动。也就是说问题出在新鲜度上。那是什么造成的呢？spec的变更应该会产生一个事件，被controller和informer消费掉，这样它们才能更新自己的状态。如果往代码深处看，会发现informer往它们发事件的顺序是不固定的——这是对的，而且是刻意为之，因为这样它就不会卡在某个很慢的消费者上。再往下翻handler和controller的代码，我发现discovery controller负责发布discovery document。它应该是很关键的一环，因为原始issue里描述的症状就是url对不上。但它的逻辑是没问题的。\n于是我把注意力放到crdHandler上，它才是真正管理CRD REST storage的那个handler。然后我找到了根因：这个handler用crd.UID去一个可能已经过期的cache里查，而且完全没有检查cache是不是最新的。所以当Informer把事件同时发给discovery controller和crdHandler时，race就出现在这里。如果discovery controller比crdHandler快，那么请求到达的时候，crdHandler手里还拿着一份过期的cache，根本不知道spec已经变了。反过来的顺序才是正常、符合预期的路径。\n问题找到了，这其实是最难的部分。修法就很清楚了。既然我们不能去改informer的行为，那样影响面太大，那能做的就是让crdHandler在spec变化时更新自己的store。所以我把用crd.UID查cache map的那段逻辑去掉了，改成用generation来判断spec有没有变过。\n这里有个坑。generation好用，但还不够。如果expectedName（在crd status里）和别的冲突了，而这种情况server更新时是不会bump generation的，crdHandler就不会知道，可能会构建出错误的storage。所以我们还需要加一个函数来比较它们是否相等，用来判断要不要更新storage。\n","permalink":"https://blog.datayurei.com/posts/k8s-issue-130235/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003e本文最初以英文写成。\u003c/em\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这个issue和controller以及handler有关，而这部分代码我并不熟悉，所以花了我好几天才搞明白crd server内部到底是怎么跑起来的。\u003c/p\u003e","title":"顺着issue#130235理解CRD server是怎么工作的"},{"content":" 本文最初以英文写成。\n我修的这个issue和kubernetes的内存watch cache有关。这部分代码负责维护一份内存里的缓存（我觉得它是用ring buffer实现的），并且在etcd发生变化时保持同步。问题的源头在staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go里watch()函数的这一行：\ngo watcher.processInterval(ctx, cacheInterval, requiredResourceVersion) 这一行在函数快结束的位置，而且可以说是最重要的部分：它起了一个goroutine来让cache watcher保持同步。它前面的那些东西——取RV、配置options、检查feature gates——都只是准备工作。\n回到这个issue：它说明某一类特定的客户端会导致api server的watch实现里出现goroutine泄漏。复现代码是这样的：\nsendInitialEvents := true timeoutSeconds := int64(time.Hour.Seconds()) watcher, _ := client.CoreV1().Pods(namespace).Watch(ctxOnce, metav1.ListOptions{ ResourceVersion: \u0026#34;0\u0026#34;, ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, SendInitialEvents: \u0026amp;sendInitialEvents, Watch: true, AllowWatchBookmarks: true, TimeoutSeconds: \u0026amp;timeoutSeconds, }) defer watcher.Stop() for { select { case \u0026lt;-watcher.ResultChan(): // do not deal with event time.Sleep(time.Hour) case \u0026lt;-ctx.Done(): return } } 这段代码跑了一个每小时才活跃一次的watch客户端。有个细节值得注意，就是那个\u0026quot;0\u0026quot;的resource version——它请求的是完整历史。\n下一步是去看底层那个goroutine函数，也就是staging/src/k8s.io/apiserver/pkg/storage/cacher/cache_watcher.go里的processInterval。这个函数收到一个cacheInterval，里面有source字段、它的迭代逻辑、一个resourceVersion，还有一个initialEventsEndBookmark。这个类型是用来消化cache和etcd之间的那段间隔的。\n那到底是什么让它卡住的？再往下挖就能找到这个：\nc.sendWatchCacheEvent(event) 它里面是：\nselect { case c.result \u0026lt;- *watchEvent: c.markBookmarkAfterRvSent(event) sentAt = c.clock.Now() case \u0026lt;-c.done: } 这就是问题的源头：一旦客户端不再从channel里消费，这个select就会阻塞在c.result \u0026lt;- *watchEvent这个case上——复现代码里正是这种情况，watch每小时才读一次。\n有了这些，我们就可以从更大的视角看整条控制路径。Watch准备好cache watch，然后调用processInterval，主要的watch和同步逻辑都在里面。它内部有两个阶段。第一个阶段，对于还处在initial阶段的请求，它会通过sendWatchCacheEvent把etcd里的完整历史回放到c.result里。如果客户端不再消费这些事件，就会造成issue里描述的那种卡死。\n搞清楚全貌之后，下一个问题就是怎么修。我最初的想法来自issue下面的一条评论，就是把那些已经不在watch cache里的引用清掉。但我很快就发现这么做很麻烦而且不值得——大概需要给每个event/snapshot加上反向引用，删除的时候才能回溯，而且这是热路径上的操作，可能会影响性能。\n所以我选了更简单的做法：用一个固定的超时时间——30秒——如果一个watcher在initial阶段发送事件没有任何进展就把它干掉。每次成功发送都会重置计时器，所以它限制的只是一个watch能完全卡住多久，而不是把所有东西发完要多久。这样可以保护那些慢但是一直在推进的watcher，包括initial snapshot很大的那些，不会被不公平地清掉——如果用一个总时长预算就会有这种风险。这个改动能修好复现代码，也解决了连接卡死的问题。\n注意processInterval最后会调用process，也就是说这个连接已经走完initial阶段，进入streaming阶段了。因为process里也调用了同一个sendWatchCacheEvent，我一开始以为我的修改也会影响到它——于是又往下挖了挖。结果发现它并不需要这个修改：streaming阶段本身就有一套处理卡死的路径。当它卡住的时候，c.input这个channel会阻塞，而在生产者那一侧，add()很快就会发现这一点并把卡住的watcher关掉。所以它不需要超时这个修复。\nPR: https://github.com/kubernetes/kubernetes/pull/141899\nIssue: https://github.com/kubernetes/kubernetes/issues/131721\n","permalink":"https://blog.datayurei.com/posts/k8s-issue-131721/","summary":"解决k8s的issue 131721时学到的一些关于cache机制的东西","title":"解决issue#131721时学到的一些kubernetes api-server watch机制的东西"},{"content":"黑夜君临在linux上运行的帧数异常的低，而且cpu和gpu的占用率都很低，google了一番于是在github proton issue里找到了答案\nhttps://github.com/ValveSoftware/Proton/issues/8751#issuecomment-2957426376\nhttps://www.phoronix.com/news/Linux-Splitlock-Hurts-Gaming\n因为某些游戏或反作弊/驱动/运行库会触发 split lock，Linux 检测到后会对相关进程限速，避免它拖慢整机。\n这个机制对系统公平性有利，但对游戏来说就有可能表现为帧率下降、卡顿、加载变慢或输入延迟增加。\n关闭 kernel.split_lock_mitigate=0 后，内核不再处罚触发进程，所以游戏就能恢复性能；代价是如果程序继续大量触发 split lock，可能拖慢整个系统的响应。\n对于日用的linux pc来说我认为是完全可以关掉的并且不会有太大影响。\n具体的修复方法如下（一次性\u0026amp;立刻生效）\nsudo sysctl kernel.split_lock_mitigate=0 如果想要恢复原来的就改为1就行了\nsudo sysctl kernel.split_lock_mitigate=1 持久化需要向/etc/sysctl.d/99-splitlock.conf中添加\nkernel.split_lock_mitigate=0 ","permalink":"https://blog.datayurei.com/posts/game-optimization-on-linux/","summary":"linux下由split lock mitigation引起的游戏性能问题，以及怎么绕过它","title":"个别游戏如艾尔登法环黑夜君临在linux上的帧数问题"},{"content":"这几天由于在windows上碰到了诸多小问题，打算把主力系统换成ubuntu，于是就开始折腾起了如何在linux上玩游戏。\n当我测试到helldivers2的时候发现全屏没法像其他游戏那样正常实现，具体表现就是全屏后waybar还在显示，哪怕把waybar关闭了\n也会出现屏幕上面或者下面还会能看到一点点桌面的问题，想使用gamescope又会崩溃或者干脆开不起来。\n直到有一次偶然间错把niri的scale重置成了1，才发现原来是fractional scale的问题。只要把scale调回整数一切就迎刃而解了。\n虽然wayland在今天已经相当可用了，不过分数缩放这种功能支持还不是非常完美，尽量还是能不开就不开。\n","permalink":"https://blog.datayurei.com/posts/wayland-niri-gaming-fullscreen-bug/","summary":"有关linux下使用wayland和niri运行全屏游戏时出现无法全屏的问题","title":"使用niri玩游戏时的全屏问题"},{"content":"这是一篇用于验证 Hugo 与 GitHub Actions 部署流程的初始文章。\n","permalink":"https://blog.datayurei.com/posts/hello-hugo/","summary":"用于验证 Hugo 与 GitHub Actions 部署流程的初始文章。","title":"你好，Hugo"}]