<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>詠夏日記</title><description>Blog</description><link>https://yomi.moe/</link><language>zh_CN</language><item><title>2025 年终总结</title><link>https://yomi.moe/posts/2025/</link><guid isPermaLink="true">https://yomi.moe/posts/2025/</guid><description>嘛……总之，是一篇图很多的流水账。</description><pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;多图预警（&lt;/p&gt;
&lt;p&gt;直接开始吧。&lt;/p&gt;
&lt;h1&gt;生活&lt;/h1&gt;
&lt;p&gt;今年一年的社交次数，应该比我此前数年加起来都多。&lt;/p&gt;
&lt;p&gt;也算是完成了去年给自己定下的目标？像是被迫进行某种社会化实验。&lt;/p&gt;
&lt;p&gt;总之是在不断被推着走向现实中。换句话说，也就是越来越像人类了（&lt;/p&gt;
&lt;h2&gt;上半年&lt;/h2&gt;
&lt;p&gt;一月，来新加坡读书了。&lt;/p&gt;
&lt;p&gt;落地之后急匆匆地安顿下来，也就回到了熟悉的生活节奏。除了物价高和语言环境的少许变化外，似乎和在国内的生活也没什么差别了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./air.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在坡也是基本没什么社交。随后就是持续四个月的闭关学习生活，每天图书馆出租屋两点一线，做题、看面经、背八股。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./hive.webp&quot; alt=&quot;夜拍The Hive&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;夜拍 The Hive&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;暑期实习面试过程不怎么顺利，但最终好歹是拿到了 offer，算是给这学期一个交代了。&lt;/p&gt;
&lt;p&gt;六月，来沪实习了。&lt;/p&gt;
&lt;h2&gt;下半年&lt;/h2&gt;
&lt;p&gt;实习期间正好和一位本科舍友聚了一下。
久违的线下聊天，话题绕不开现在低迷的行情、身边惨兮兮的学长学姐和未来打算如何体面地躺平（&lt;/p&gt;
&lt;p&gt;以后应该都会在各自的社畜道路上一去不还吧，下次再见不知道是何时了。&lt;/p&gt;
&lt;p&gt;吃的是地道儿老北京火锅，肉质不赖，好食！&lt;/p&gt;
&lt;p&gt;:spoiler[&lt;s&gt;图找不到了，就这样吧（&lt;/s&gt;]&lt;/p&gt;
&lt;p&gt;八月初请了两天假，和同音圈的朋友面基（应该也是我第一次线下？）&lt;/p&gt;
&lt;p&gt;见面相约纯K，嗯唱三小时。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./take-no-hana.webp&quot; alt=&quot;竹ノ花&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/MQtw6W03IKw?si=gAjVxxcZcqBBMgeJ&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;唱了乖離光，全程口胡（&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./kairikou.webp&quot; alt=&quot;乖離光&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/GMGOLjwWu3o?si=IsMGt2ZA1oNSFWFb&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;说起来，在外面住的时候房间隔音都很差，实在没什么唱歌的机会，算是久违放松一下了（&lt;/p&gt;
&lt;p&gt;第二天，来 animate 的车万联动，买色素水送时尚小垃圾系列（:spoiler[真的很难喝，啥比二刺螈的钱真好骗（]&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./animate.webp&quot; alt=&quot;静安大悦城 animate&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;上海静安大悦城 animate cafe&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接着去打卡雾雨咖啡店，正值沪 t 前，店里店外都是人头攒动。饮品这块只能说比animate值太多了（&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./kirisame-coffee.webp&quot; alt=&quot;雾雨咖啡店&quot; /&gt;&lt;/p&gt;
&lt;p&gt;晚上和朋友一起去了幽闭星光的live（也是第一次看live）&lt;/p&gt;
&lt;p&gt;虽说我平时基本不会主动听他们的歌，但毕竟是名社团，选曲像是华鸟风月、大地咲，大部分都是我听过的名曲。&lt;/p&gt;
&lt;p&gt;现场极其闷热算是扣分点（ 算是一次初体验，总体也是不错的吧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./yuuhei-satellite.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2025.8.8  東方樂典Special 幽閉サテライトSOLOLIVE&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;第三天，去了魅知幻想博览会，氛围真好啊。:spoiler[对我这种阴暗肥宅来说，去各个摊位到处钻还是太超纲了）]&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./fumos.webp&quot; alt=&quot;一堆fumos&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一堆 fumos&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./fumos2.webp&quot; alt=&quot;fumos2&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;奶龙小五（&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./hand-painted.webp&quot; alt=&quot;hand-painted&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;签绘墙&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;打卡沪上光棱塔。:spoiler[那两天没喝蜜雪冰城是正确的（]&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./oriental-pearl-tower.webp&quot; alt=&quot;东方明珠&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;东方明珠&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;之后又回归了临时社畜的生活。白天对着电脑搬砖，下班失去学习动力，狠狠放松（&lt;/p&gt;
&lt;p&gt;九月初离职了。Leader 人很好，感恩团队这段时间对我的包容。&lt;/p&gt;
&lt;p&gt;在线下走完秋招的第一场完整流程后，在上海这边的任务也算告一段落。回程途中多做了两站，去南京路漫无目的地逛了逛，然后又去了一次雾雨咖啡店，尝了尝新品（&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./kirisame-coffee2.webp&quot; alt=&quot;雾雨咖啡店&quot; /&gt;&lt;/p&gt;
&lt;p&gt;听说最近装修了，也算是在记忆里留下了旧店面的样子。&lt;/p&gt;
&lt;p&gt;回到新加坡，有offer保底了，心态从容了不少，有闲暇去做点其他事了。&lt;/p&gt;
&lt;p&gt;约了恰好同校，这学期就要毕业的同音同好出来吃个饭。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./itsuki-and-misuzu.webp&quot; alt=&quot;棗和美铃nunu&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;:spoiler[别拍你那棉花娃娃了！]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;十一月，去参加 Google 的 DevFest SG！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./devfest1.webp&quot; alt=&quot;devfest1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;英语太差 + 社恐，最终也没怎么敢社交。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./devfest2.webp&quot; alt=&quot;devfest2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一次参加这种活动，感觉总体来说 Topic 都是挺大众的。以后类似的机会应该还会尝试，权当是锻炼社交耐受力了，，，&lt;/p&gt;
&lt;p&gt;顺便继续坐几站地铁，打卡鱼尾狮公园。一时兴起来的，没去滨海湾花园，明年再来一次吧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./marina.webp&quot; alt=&quot;滨海湾&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;滨海湾&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./merlion.webp&quot; alt=&quot;鱼尾狮公园&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;鱼尾狮公园&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;之后也就没什么事了，如同往常一样，看面经、背八股，不同的是有了 offer ，可以尝试着劳逸结合了（&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./sichuan.webp&quot; alt=&quot;蜀香坊&quot; /&gt;&lt;/p&gt;
&lt;p&gt;考试后和新加坡的要毕业的朋友们聚餐告别，这学期就这样结束了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./jd.webp&quot; alt=&quot;offer&quot; /&gt;&lt;/p&gt;
&lt;p&gt;秋招的结果之一，算是差强人意，不过也总算有个确定的起点，春招再战吧。&lt;/p&gt;
&lt;h1&gt;游戏&lt;/h1&gt;
&lt;p&gt;今年比起往年来玩过的游戏少了很多，一方面是因为成为了社畜预备役，另一方面也是因为今年确实没什么感兴趣的新游戏。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./steam.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首先是 JRPG 的名作重置版，空之轨迹1st：扎实的剧情和及其舒适的游戏体验，JRPG 就得这样做才对味嘛。&lt;/p&gt;
&lt;p&gt;ENDER MAGNOLIA：整体的氛围远不如前作，而单纯从 gameplay 来看质量又属实一般，虽然画面、手感等方面相对前作进步，但瑜不掩瑕，失望了（&lt;/p&gt;
&lt;p&gt;莫莫多拉:月下告别：游戏内容量太小导致品不出类银味，游戏本身还可以，但不值这个价格（&lt;/p&gt;
&lt;p&gt;怪物猎人荒野：没怎么玩，不评价了（&lt;/p&gt;
&lt;p&gt;斗技场的阿利娜：底子很好的游戏，将战棋+卡牌融合的很棒，虽然体量也小，但好在不贵，推荐。&lt;/p&gt;
&lt;p&gt;魔法少女的魔女审判：剧情一般但却相当有趣的游戏，看得出在人设和剧本编排上非常懂如何抓住当今玩家的喜好，是一款除了程序都蛮完善的商业作品。&lt;/p&gt;
&lt;p&gt;空洞骑士丝之歌：:spoiler[我说工具毁了丝之歌（]体量变大了，但地图资源的编排也更乱，导致游玩起来的垃圾时间挺多，除此之外总结优点的话和前作也没什么不同，总得来说比较微妙。&lt;/p&gt;
&lt;p&gt;逃离鸭科夫：小品游戏，还挺上头的。&lt;/p&gt;
&lt;p&gt;星之终途：老套的设定也可以写出好故事。&lt;/p&gt;
&lt;p&gt;Lunaria：月乃唱的 OP 不错，ふむゆん画得也很可爱，此外没什么印象了（&lt;/p&gt;
&lt;p&gt;弹丸论破：趁着打折 4r 购入！说实话蛮一般的，可能是因为是十五年前的游戏吧，略显平淡了。&lt;/p&gt;
&lt;p&gt;弹丸论破2：神作。:spoiler[推理部分比一代强五个魔裁（]&lt;/p&gt;
&lt;p&gt;杀戮尖塔：我怎么还在玩这个。&lt;/p&gt;
&lt;p&gt;赛菲利亚：背包管理没有贪婪地牢玩起来无脑了，不过支持联机是个大加分项，而且正式版看起来应该会体量不小，后面应该会是个解压爽游。&lt;/p&gt;
&lt;p&gt;此外，还入坑鸣潮了，运气实在太好（&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./wuwa.webp&quot; alt=&quot;鸣潮&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;音乐&lt;/h1&gt;
&lt;p&gt;今年大部分时间都在坡，基本都是直接在油管上听。网易云没怎么用，年度总结也就没什么参考意义了。&lt;/p&gt;
&lt;p&gt;这部分就简单记录下今年的听的歌姬：&lt;/p&gt;
&lt;p&gt;首先是 すずみしろ。:spoiler[关注 すずしろ 喵，关注 すずしろ 谢谢喵（]&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./suzumishiro.webp&quot; alt=&quot;Youtube recap&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上半年在李伟男图书馆，基本都在用凉白的录播当背景音。&lt;/p&gt;
&lt;p&gt;然后是 るる 还有薄明学生会啦。るる的声音很有磁性且抓耳，肯定早晚会大火的！&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/1UpFa3Ai7zk?si=1OguNlIVOOgaamIc&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;然后就是神椿的御莉姫 -:spoiler[&lt;s&gt;G&lt;/s&gt;]ORIHIME-&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/_up8PpOBsyI?si=pOXqWYdS1tvwmbEo&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/cHtm0g0o1cY?si=OHQokNjpDwxyMwV8&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;神一般的高音。。&lt;/p&gt;
&lt;p&gt;最后是&lt;s&gt;藍月なくる&lt;/s&gt;Endorfin.&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/9LRLn4Ku1bw?si=l-nOncYk1-QS-Gsb&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
这首 Dream Blue 莫名让人怀念（&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/3RuCaE5ciNU?si=Wwg2Db1MpNbtko32&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
从 17 年第一次在网易云上听 Horizon Note 入坑，到如今十周年的 live，转眼间近九年的时光已逝，一切都切实改变了太多，好在 horizon 的调性仍在。&lt;/p&gt;
&lt;h1&gt;2026&lt;/h1&gt;
&lt;h2&gt;回顾&lt;/h2&gt;
&lt;p&gt;在为未来做打算前，先回顾下去年的 Flag 倒了多少吧（&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 彻底不看国内平台的垃圾内容&lt;/li&gt;
&lt;li&gt;[x] 再多结交一些志同道合的朋友 :spoiler[算是超额完成？这点要求不能太高（]&lt;/li&gt;
&lt;li&gt;[ ] 多做一些有价值的开源&lt;/li&gt;
&lt;li&gt;[x] 找个大厂实习（:spoiler[达成了，感恩 mentor 的包容]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于第一点，应该没什么办法，审丑文化本身就是顺应这个碎片化时代而繁荣的，掩耳盗铃的被动防御作用终归有限。比起强迫戒断，真正有效的方式或许是尝试从消费者到生产者的身份转变吧。这当然不是说什么打不过就加入（，而是说将娱乐的重心从 be fed 转变为 to create。&lt;/p&gt;
&lt;p&gt;至于开源，现在想来，我当初的出发点应该是想把它作为一种 energeia？但现在发现，这种能量的获取也许不必强求于此。总之，下次一定（&lt;/p&gt;
&lt;h2&gt;未来&lt;/h2&gt;
&lt;p&gt;不出意外，26 年就要正式开始社畜生涯了。&lt;/p&gt;
&lt;p&gt;过去的每一个抉择，似乎都只是为了逃离某种痛苦。我曾以为这些自审式的折磨是前进的唯一动力，但当那份压力暂时消失后，我反而失去了新的目标。&lt;/p&gt;
&lt;p&gt;回过头来看，过去一年甚至几年的抉择，很多时候都源自带有逃避色彩的固执。而对于好不容易到达的终点，我谈不上不满，但也并没有对未来产生多少期盼。我的内心戏总是比做的要多得多，或许真正入职以后，这种的状态会迎来某些转机吧，亦或许这种机械式的前进大概也算是非正常人类的常态。&lt;/p&gt;
&lt;p&gt;在 DevFest 上听一位 presenter 说，他的代码水平完全不如在座的各位，但这不是问题，他借助 AI 去实现自己的想法。也确实如此，技术正在 AI 的加持下变得越来越廉价。以前 Linus 说 “Talk is cheap, show me the code”，但现在跑通一个 Demo 似乎也成了顺手而为的事。代码本身在贬值，验证过程和系统的稳定性反而成了更难跨过的坎。如何在这个 AI 时代重新锚定自己的价值，可能也是明年乃至后面数年需要面对的问题。&lt;/p&gt;
&lt;p&gt;丧气话说到这里，给明年也定点目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;よく食べ、よく寝る&lt;/li&gt;
&lt;li&gt;暂定一个位置，明年日后再议吧&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上，感谢看到这里。&lt;/p&gt;
</content:encoded></item><item><title>Golang context 包实现详解</title><link>https://yomi.moe/posts/go-context/</link><guid isPermaLink="true">https://yomi.moe/posts/go-context/</guid><description>简单梳理下 Golang 的 context 包的实现逻辑</description><pubDate>Tue, 09 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;context&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;context&lt;/code&gt;用于在 goroutine 间传递特定数据、取消信号等上下文信息。一般用于服务器中的请求链路中，实际应用示例不在本文赘述。&lt;/p&gt;
&lt;p&gt;本文基于 &lt;strong&gt;Go 1.24.5&lt;/strong&gt; 的源码，从实现角度逐层分析 &lt;code&gt;context&lt;/code&gt; 的结构设计与取消机制。&lt;/p&gt;
&lt;h2&gt;context 接口定义&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Context&lt;/code&gt; 是一个抽象接口，定义了取消、超时和键值传递的统一语义，所有具体类型（如 &lt;code&gt;cancelCtx&lt;/code&gt;、&lt;code&gt;timerCtx&lt;/code&gt;）都是它的实现。&lt;/p&gt;
&lt;p&gt;&amp;lt;pre&amp;gt;
Context interface
├── emptyCtx
│   ├── backgroundCtx
│   └── todoCtx
├── cancelCtx
│   └── timerCtx
└── valueCtx
&amp;lt;/pre&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Context interface {
    // Deadline 返回 context 被取消的时间
    // 返回 false 代表无 deadline，不会自动取消
    Deadline() (deadline time.Time, ok bool)

    // Done 返回一个 channel，当 context 被取消，即工作完成时，会关闭这个 channel
    // 若 context 不会被取消，Done 返回 nil 
    // Done 关闭可能会与 cancel 函数的返回异步发生
    Done() &amp;lt;-chan struct{}

    // 在 Done 被关闭后，返回 context 的取消原因
    Err() error

    // Value 返回 context 中对应键中存储的值，若键不存在则返回 nil
    Value(key any) any
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;emptyCtx: TODO/Background&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;emptyCtx&lt;/code&gt; 是最简单的 &lt;code&gt;context&lt;/code&gt;接口实现，永不取消，不存储键值对，无 deadline。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// backgroundCtx 与 todoCtx 在实现上都是 emptyCtx，仅有 String() 一处不同
type emptyCtx struct{}  

func (emptyCtx) Deadline() (deadline time.Time, ok bool) {  
    return  
}  

func (emptyCtx) Done() &amp;lt;-chan struct{} {  
    return nil  
}  

func (emptyCtx) Err() error {  
    return nil  
}  

func (emptyCtx) Value(key any) any {  
    return nil  
}  

type backgroundCtx struct{ emptyCtx }  

func (backgroundCtx) String() string {  
    return &quot;context.Background&quot;  
}  

type todoCtx struct{ emptyCtx }  

func (todoCtx) String() string {  
    return &quot;context.TODO&quot;  
}

// Background 用于创建最顶层 context
func Background() Context {
    return backgroundCtx{}
}

// TODO 用于不确定需要使用何种 context 时作为占位符
func TODO() Context {
    return todoCtx{}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;WithCancel&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;WithCancel&lt;/code&gt; 是 &lt;code&gt;context&lt;/code&gt; 的核心构造函数之一，它返回一个可手动取消的 &lt;code&gt;context&lt;/code&gt;。其底层依赖 &lt;code&gt;cancelCtx&lt;/code&gt; 来实现取消传播。&lt;code&gt;cancelCtx&lt;/code&gt;是带 cancel 功能的 &lt;code&gt;context&lt;/code&gt;，在调用 &lt;code&gt;cancel()&lt;/code&gt; 时也会同时取消其子 &lt;code&gt;context&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;cancelCtx&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;cancelCtx&lt;/code&gt;中嵌套了一个 &lt;code&gt;Context&lt;/code&gt;的匿名字段，这个字段代表其父节点。同时，&lt;code&gt;cancelCtx&lt;/code&gt;本身也实现了&lt;code&gt;Context&lt;/code&gt;接口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type cancelCtx struct {
     Context // 嵌入其父 ctx
     mu       sync.Mutex            // 互斥锁，保证线程安全
     done     atomic.Value          // 存储一个 chan struct{}, 懒加载，在调用 cancel() 时关闭
     children map[canceler]struct{} // 子 ctx 的集合，在 cancel() 被调用时设为 nil
     err      error                 // 在 cancel() 被调用时设置
     cause    error                 // 取消原因
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// cancelCtxKey 是一个 int 指针类型的全局变量，用于判断当前 ctx 是否为 cancelCtx类型
// 返回最早的 cancelCtx
func (c *cancelCtx) Value(key any) any {
    // 返回自身
    if key == &amp;amp;cancelCtxKey {
        return c
    }
    return value(c.Context, key)
}

// 当找不到 &amp;amp;cancelCtxKey 时会继续调用 value(c.Context, key) 向上寻找
func value(c Context, key any) any {
    for {
        switch ctx := c.(type) {
        case *valueCtx:
            if key == ctx.key {
                return ctx.val
            }
            c = ctx.Context
        case *cancelCtx:
            if key == &amp;amp;cancelCtxKey {
                return c
            }
            c = ctx.Context
        case withoutCancelCtx:
            if key == &amp;amp;cancelCtxKey {
                return nil
            }
            c = ctx.c
        case *timerCtx:
            if key == &amp;amp;cancelCtxKey {
                return &amp;amp;ctx.cancelCtx
            }
            c = ctx.Context
        case backgroundCtx, todoCtx:
            return nil
        default:
            return c.Value(key)
        }
    }
}

// 懒创建并返回 done
func (c *cancelCtx) Done() &amp;lt;-chan struct{} {
    d := c.done.Load()
    if d != nil {
        return d.(chan struct{})
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    d = c.done.Load()
    if d == nil {
        d = make(chan struct{})
        c.done.Store(d)
    }
    return d.(chan struct{})
}

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    err := c.err
    c.mu.Unlock()
    return err
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cancelCtx&lt;/code&gt; 不仅实现了 &lt;code&gt;Context&lt;/code&gt;，同时还实现了 &lt;code&gt;canceler&lt;/code&gt;接口，代表可取消的 ctx：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// cancelCtx 和 timerCtx 都实现了 canceler 接口
type canceler interface {  
    cancel(removeFromParent bool, err, cause error)  
    Done() &amp;lt;-chan struct{}  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 children 中存储的就是当前 &lt;code&gt;cancelCtx&lt;/code&gt; 的最近的子 &lt;code&gt;cancelCtx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在创建 &lt;code&gt;cancelCtx&lt;/code&gt;的同时，定义 &lt;code&gt;cancel&lt;/code&gt;函数，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// WithCancel 返回一个带有新 Done channel 的派生 context
// 当父 context 被取消或当前 context 被取消时，Done 都会被关闭
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := withCancel(parent)
    return c, func() { c.cancel(true, Canceled, nil) }
}

func withCancel(parent Context) *cancelCtx {
    if parent == nil {
        panic(&quot;cannot create context from nil parent&quot;)
    }
    c := &amp;amp;cancelCtx{}
    c.propagateCancel(parent, c)        // 建立父子关系
    return c
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;cancel()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cancel()&lt;/code&gt; 会关闭自身的 &lt;code&gt;done&lt;/code&gt;，设置 &lt;code&gt;cause&lt;/code&gt;，递归通知所有子 ctx，然后将当前的 ctx 从父节点中移除。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 关闭 c.done,取消 c 的所有 children
// 若 removeFromParent 为 true, 把 c 从它父节点的 children 中移除
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
    if err == nil {
        panic(&quot;context: internal error: missing cancel error&quot;)
    }
    if cause == nil {
        cause = err // cause 未设置，取 err
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // context 已取消
    }
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
        c.done.Store(closedchan) // 存 closedchan 作为已取消的标志
    } else {
        close(d) // 如果 done 已被设置，直接关闭 d
    }
    // 递归取消所有的子 context
    for child := range c.children {
        child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent { // 移除当前子 context
        removeChild(c.Context, c)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;removeChild()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;removeChild()&lt;/code&gt; 的作用是寻找 &lt;code&gt;parent&lt;/code&gt; 的 &lt;code&gt;cancelCtx&lt;/code&gt; 类型的祖先 &lt;code&gt;p&lt;/code&gt;，并维护 &lt;code&gt;p&lt;/code&gt; 的 &lt;code&gt;children&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func removeChild(parent Context, child canceler) {
    if s, ok := parent.(stopCtx); ok { // 解绑回调函数
        s.stop()
        return
    }
    p, ok := parentCancelCtx(parent)
    if !ok {
        return
    }
    p.mu.Lock()
    if p.children != nil {
        delete(p.children, child) // 在祖先 ctx 的 children 中删除 child
    }
    p.mu.Unlock()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;parentCancelCtx()&lt;/h3&gt;
&lt;p&gt;一直向上寻找最近的 &lt;code&gt;cancelCtx&lt;/code&gt; 祖先，检验后返回。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func parentCancelCtx(parent Context) (*cancelCtx, bool) {
     done := parent.Done()
     if done == closedchan || done == nil {
         return nil, false
     }
     p, ok := parent.Value(&amp;amp;cancelCtxKey).(*cancelCtx)
     if !ok {
         return nil, false
     }
     pdone, _ := p.done.Load().(chan struct{})
     if pdone != done {
         return nil, false
     }
     return p, true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;propagateCancel&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;propagateCancel&lt;/code&gt;的作用一言以蔽之就是将 parent 注册为 c 的父节点，维护祖先 &lt;code&gt;cancelCtx&lt;/code&gt; 的 children，将 c 放入到 children 中，形成父链；如果没有找到祖先，则另起额外的协程监听 c 的取消。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
    c.Context = parent

    done := parent.Done()
    if done == nil {
        return // 父 context 的 done 为 nil，不会 cancel
    }

    select {
    case &amp;lt;-done:
        // 父 context 已经被取消，接着取消 child
        child.cancel(false, parent.Err(), Cause(parent))
        return
    default:
    }

    if p, ok := parentCancelCtx(parent); ok {
        // 若祖先链路上存在 *cancelCtx p
        p.mu.Lock()
        if p.err != nil {
            // p 已经被取消
            child.cancel(false, p.err, p.cause)
        } else {
            // 将当前 cancelCtx 注册为 p 的子 cancelCtx
            // 也就是说，cancelCtx 单独维护一条链，记录 cancelCtx 间的祖先关系，以此减少遍历经过的节点数
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
        return
    }

    if a, ok := parent.(afterFuncer); ok { 
    // 如果父 context 实现了 AfterFunc 方法
        c.mu.Lock()
        // 为这个父 context a 注册一个 afterFunc
        stop := a.AfterFunc(func() {
            child.cancel(false, parent.Err(), Cause(parent))
        })
        // 中间插入一个 stopCtx 节点
        c.Context = stopCtx{
            Context: parent,
            stop:    stop,
        }
        c.mu.Unlock()
        return
    }

    // 起一个协程监听父 context 是否被取消，以判断取消 child
    goroutines.Add(1)
    go func() {
        select {
        case &amp;lt;-parent.Done():
            child.cancel(false, parent.Err(), Cause(parent))
        case &amp;lt;-child.Done():
        }
    }()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;afterFuncCtx/stopCtx&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// AfterFunc 是 Go 1.21 引入的方法，作用是在 context 被取消时调用函数 f 

// 返回的 stop 函数可以终止 ctx 和 f 的关联
func AfterFunc(ctx Context, f func()) (stop func() bool) {
    a := &amp;amp;afterFuncCtx{
        f: f,
    }
    a.cancelCtx.propagateCancel(ctx, a)
    return func() bool {
        stopped := false
        a.once.Do(func() {
            stopped = true
        })
        if stopped {
            a.cancel(true, Canceled, nil)
        }
        return stopped
    }
}

type afterFuncer interface {
    AfterFunc(func()) func() bool
}

type afterFuncCtx struct {
    cancelCtx
    once sync.Once
    f    func()
}

func (a *afterFuncCtx) cancel(removeFromParent bool, err, cause error) {
    a.cancelCtx.cancel(false, err, cause)
    if removeFromParent {
        removeChild(a.Context, a)
    }
    a.once.Do(func() {
        go a.f()
    })
}

// stopCtx 只用于在 cancelCtx 祖先链上无 cancelCtx 但存在 afterFunc 时，充当该 ctx 的父节点
// 调用 stop() 后，再取消 ctx 不会再执行 AfterFunc 注册的 f
type stopCtx struct {
    Context
    stop func() bool
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WithCancelCause&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;WithCancelCause&lt;/code&gt; 类似 &lt;code&gt;WithCancel&lt;/code&gt;，不同在于返回值是 &lt;code&gt;CancelCauseFunc&lt;/code&gt; 而不是 &lt;code&gt;CancelFunc&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// CancelCauseFunc 相比 CancelFunc 增加了一个取消原因，使用时需要传入一个 error 参数
// 可以调用 Cause 来获取具体的原因
type CancelCauseFunc func(cause error)

func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
    c := withCancel(parent)
    return c, func(cause error) { c.cancel(true, Canceled, cause) }
}

// 如果 CancelCauseFunc(err) 设置过取消原因，则返回这个原因
// 否则返回 c.Err()
func Cause(c Context) error {  
    if cc, ok := c.Value(&amp;amp;cancelCtxKey).(*cancelCtx); ok {  
       cc.mu.Lock()  
       defer cc.mu.Unlock()  
       return cc.cause  
    }  
    return c.Err()  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WithoutCancel&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;WithoutCancel&lt;/code&gt; 返回一个 &lt;code&gt;withoutCancelCtx&lt;/code&gt;，它不会继承父节点的取消语义。除了 &lt;code&gt;Value()&lt;/code&gt; 与 &lt;code&gt;String()&lt;/code&gt; 以外其他方法行为与 &lt;code&gt;emptyCtx&lt;/code&gt; 相同。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 
func WithoutCancel(parent Context) Context {
    if parent == nil {
        panic(&quot;cannot create context from nil parent&quot;)
    }
    return withoutCancelCtx{parent}
}

type withoutCancelCtx struct {
    c Context
}

func (withoutCancelCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (withoutCancelCtx) Done() &amp;lt;-chan struct{} {
    return nil
}

func (withoutCancelCtx) Err() error {
    return nil
}

func (c withoutCancelCtx) Value(key any) any {
    return value(c, key)
}

func (c withoutCancelCtx) String() string {
    return contextName(c.c) + &quot;.WithoutCancel&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;timerCtx&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;timerCtx&lt;/code&gt; 内部嵌入了一个匿名字段 &lt;code&gt;cancelCtx&lt;/code&gt;。与 &lt;code&gt;cancelCtx&lt;/code&gt; 中嵌入的 &lt;code&gt;Context&lt;/code&gt; 字段不同，这里的嵌入并不是为了保存父节点，而更像是一种面向对象意义上的“继承”——&lt;code&gt;timerCtx&lt;/code&gt; 通过组合 &lt;code&gt;cancelCtx&lt;/code&gt; 来复用其取消逻辑与状态字段。真正的父节点并不保存在 &lt;code&gt;timerCtx&lt;/code&gt; 本身，而是位于其内部的 &lt;code&gt;cancelCtx.Context&lt;/code&gt; 字段中，也就是说，&lt;code&gt;c.cancelCtx.Context&lt;/code&gt; 才是 &lt;code&gt;timerCtx c&lt;/code&gt; 对应的父 &lt;code&gt;context&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 当触发取消时，timerCtx 会先停止自身的定时器，然后将取消操作委托给内部的 cancelCtx.cancel()
type timerCtx struct {
    cancelCtx
    timer *time.Timer // 使用 cancelCtx.mu

    deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}

func (c *timerCtx) String() string {
    return contextName(c.cancelCtx.Context) + &quot;.WithDeadline(&quot; +
        c.deadline.String() + &quot; [&quot; +
        time.Until(c.deadline).String() + &quot;])&quot;
}

func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
    c.cancelCtx.cancel(false, err, cause)
    if removeFromParent {
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WithDeadline()&lt;/h2&gt;
&lt;p&gt;在创建&lt;code&gt;timerCtx&lt;/code&gt;时，通过&lt;code&gt;time.AfterFunc&lt;/code&gt; 注册一个定时回调来触发取消。除了另外需要做一些时间逻辑判断其他实现与 &lt;code&gt;cancelCtx&lt;/code&gt; 一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    return WithDeadlineCause(parent, d, nil)
}

func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
    if parent == nil {
        panic(&quot;cannot create context from nil parent&quot;)
    }
    if cur, ok := parent.Deadline(); ok &amp;amp;&amp;amp; cur.Before(d) {
        // 如果父节点的 deadline 更早
        return WithCancel(parent)
    }
    c := &amp;amp;timerCtx{
        deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
    dur := time.Until(d)
    if dur &amp;lt;= 0 {
        c.cancel(true, DeadlineExceeded, cause)
        return c, func() { c.cancel(false, Canceled, nil) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err.Load() == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded, cause)
        })
    }
    return c, func() { c.cancel(true, Canceled, nil) }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WithTimeout()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;WithTimeout&lt;/code&gt; 内部计算 &lt;code&gt;deadline = time.Now().Add(timeout)&lt;/code&gt;，并委托给 &lt;code&gt;WithDeadline&lt;/code&gt;创建&lt;code&gt;timerCtx&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc) {
    return WithDeadlineCause(parent, time.Now().Add(timeout), cause)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;WithValue()&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;WithValue()&lt;/code&gt; 返回一个存储提供的键值对的 &lt;code&gt;valueCtx &lt;/code&gt;，提供的 key 必须为可比较的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func WithValue(parent Context, key, val any) Context {
    if parent == nil {
        panic(&quot;cannot create context from nil parent&quot;)
    }
    if key == nil {
        panic(&quot;nil key&quot;)
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic(&quot;key is not comparable&quot;)
    }
    return &amp;amp;valueCtx{parent, key, val}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;valueCtx&lt;/h2&gt;
&lt;p&gt;每个&lt;code&gt;valueCtx&lt;/code&gt;携带一组键值对，和 &lt;code&gt;cancelCtx&lt;/code&gt;一样，嵌套一个&lt;code&gt;Context&lt;/code&gt;，但不同的是&lt;code&gt;valueCtx&lt;/code&gt;只重写了&lt;code&gt;Value()&lt;/code&gt;函数，其他函数仍使用 parent 的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type valueCtx struct {
    Context
    key, val any
}

func (c *valueCtx) String() string {
    return contextName(c.Context) + &quot;.WithValue(&quot; +
        stringify(c.key) + &quot;, &quot; +
        stringify(c.val) + &quot;)&quot;
}

func stringify(v any) string {
    switch s := v.(type) {
    case stringer:
        return s.String()
    case string:
        return s
    case nil:
        return &quot;&amp;lt;nil&amp;gt;&quot;
    }
    return reflectlite.TypeOf(v).String()
}

func (c *valueCtx) Value(key any) any {
    if c.key == key {
        return c.val
    }
    return value(c.Context, key)
}

// 不断向上寻找直至找到 key
func value(c Context, key any) any {
    for {
        switch ctx := c.(type) {
        case *valueCtx:
            if key == ctx.key {
                return ctx.val
            }
            c = ctx.Context
        case *cancelCtx:
            if key == &amp;amp;cancelCtxKey {
                return c
            }
            c = ctx.Context
        case withoutCancelCtx:
            if key == &amp;amp;cancelCtxKey {
                return nil
            }
            c = ctx.c
        case *timerCtx:
            if key == &amp;amp;cancelCtxKey {
                return &amp;amp;ctx.cancelCtx
            }
            c = ctx.Context
        case backgroundCtx, todoCtx:
            return nil
        default:
            return c.Value(key)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>我遇见的 Raft 面试题与其背后的真实问题</title><link>https://yomi.moe/posts/raft-implementation/</link><guid isPermaLink="true">https://yomi.moe/posts/raft-implementation/</guid><description>也是用 6.824 投过实习了，记录一下遇见的一些问题。</description><pubDate>Mon, 16 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;首先说一下 6.824 在我找暑期实习的过程中的共十场面试，一半问到了这个项目，其中只有两场问的比较深入，可能确实是烂大街了。还被问到过“&lt;strong&gt;你的 raft 和 etcd 的实现相比有什么优势？&lt;/strong&gt;”，我打 etcd？真的假的（x&lt;/p&gt;
&lt;p&gt;建议做6.824的同学完整读一下 Raft 作者的博士论文，并阅读一下工业 Raft 实现的代码，有时间可以尝试自己实施一下这些改进。&lt;/p&gt;
&lt;p&gt;本文记录我遇到的一些 Raft 面试题及其背后的工程语境，从实际问题出发。也借此整理一下 Raft 在真实系统中如何落地，以及有哪些被教学项目忽略的复杂度。&lt;/p&gt;
&lt;p&gt;如有错误，欢迎指出～&lt;/p&gt;
&lt;h1&gt;有看过工业上 Raft 的实现吗？比如 etcd、Ratis ....&lt;/h1&gt;
&lt;p&gt;这是我在两场面试中遇到的问题。回头来看，这类问题其实很合理：毕竟教学项目只是协议的简化实现，工业级系统往往在性能、可用性和工程复杂度上都有更高要求。&lt;a href=&quot;https://raft.github.io&quot;&gt;Raft 官网&lt;/a&gt;（raft.github.io）上列出了多个知名的开源实现，比较有代表性的包括 etcd/raft、Ratis、Dragonboat、HashiCorp 等。&lt;/p&gt;
&lt;h2&gt;etcd/raft&lt;/h2&gt;
&lt;p&gt;看一下&lt;a href=&quot;https://github.com/etcd-io/raft&quot;&gt;etcd的raft实现&lt;/a&gt;，和 MIT6.5840 给出的框架相比，最显著的差异是 etcd 的状态机是没有加锁的，而是采用了单线程事件循环，这种设计避免了并发访问状态带来的竞态与锁复杂度。由此，在事件驱动上，使得所有事件（包括状态推进、消息解析、日志提交等）都能串行执行，从而简化了并发场景下的一致性处理。&lt;/p&gt;
&lt;p&gt;说回 Raft 协议本身，其作者在他的博士论文中也坦率地指出：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unless we felt confident that a particular problem would affect a large fraction of Raft deployments, we did not address it in Raft. As a result, parts of Raft may appear naive.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，在实际环境中，根据具体场景，Raft 有大量需拓展改进的空间，而 etcd 等实现就是这些演进的优秀实例。下文我会继续整理几个典型的问题场景。&lt;/p&gt;
&lt;h1&gt;我们想要往集群内添加新的节点，该怎么做？&lt;/h1&gt;
&lt;p&gt;最直接的方法是停掉系统，更新配置，再重新上线。但这样对可用性影响太大，不太可取。&lt;/p&gt;
&lt;h2&gt;节点的动态变更&lt;/h2&gt;
&lt;p&gt;那动态添加如何添加呢？&lt;/p&gt;
&lt;p&gt;流程如下：&lt;/p&gt;
&lt;p&gt;我们对 Leader 发起添加/删除节点的命令 » 利用 Raft 机制，Leader 将变更封装为 Entry，传播给其他节点 » 过半节点获取到 Entry » Leader commit » a 节点 commit » b 节点 commit » ......&lt;/p&gt;
&lt;p&gt;这个过程中，配置切换存在不可避免的状态不一致，我们称更改前后的配置分别为 ${C_{old}}$ 和 ${C_{new}}$。&lt;/p&gt;
&lt;h3&gt;动态变更的隐患&lt;/h3&gt;
&lt;p&gt;如果我们向有三个节点的集群 ${a, b, c}$ 添加两个新节点 ${e, f}$。在某一时刻，可能出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;拥有${C_{old}}$ 节点为${{a,b}}$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拥有${C_{new}}$ 的节点为$ {{c,e,f}}$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时候，${C_{old}}$ 和 ${C_{new}}$的节点个数均大于配置记录的半数，若此时发生选举，可能会导致两个独立子集都认为自己合法，造成脑裂。&lt;/p&gt;
&lt;h3&gt;单服务器方法&lt;/h3&gt;
&lt;p&gt;论文中提出了一种策略：每次只修改一个节点，确保 $C_{old}$ 和 $C_{new}$ 的多数始终重叠，从而规避脑裂风险。
&lt;img src=&quot;./add_cluster.jpg&quot; alt=&quot;4_3.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;新节点同步滞后带来的可用性问题&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./batch_add.jpg&quot; alt=&quot;enter image description here&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Leader 将新节点先添加为无投票权成员；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新节点开始接受日志复制，追上 Leader 的 lastIndex；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;追上以后，领导者可能已经收到了新的日志条目，开始下一轮复制。轮持续时间随轮数缩短，最后一轮的持续时间少于选举超时时间；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新节点顺利完成“几轮”这样的同步过程后，就可以将新节点提权为正式节点；反之，则认为配置更改失败。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./round.jpg&quot; alt=&quot;round.jpg&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;联合共识&lt;/h2&gt;
&lt;p&gt;论文提出了一种联合共识的方法来快速配置而不是逐个删减，这种方式有着更大的灵活性，但它引入了较高的复杂实现度。&lt;/p&gt;
&lt;p&gt;核心思想：在配置从旧的 $C_{old}$ 转换为新的 $C_{new}$ 的过程中，Raft 会先进入一个中间状态 ${C_{old, new}}$ ，这个过渡配置要求新老配置节点都占大多数才算成功 commit。比如从 3 个服务器的集群更改为 9 个服务器的不同集群时，中间配置应用后继续 commit 既需要 ${C_{old}}$ 的 3 个节点中的 2 个，也需要 ${C_{old, new}}$ 的 9 个节点中的 5 个。&lt;/p&gt;
&lt;p&gt;一旦联合配置稳定运行，可以将配置更新为 $C_{new}$，此后提交只需获得 $C_{new}$ 的多数。&lt;/p&gt;
&lt;h1&gt;惊群效应&lt;/h1&gt;
&lt;p&gt;当节点断线或从集群中被删除后，会不断自增 Term， 重新连接后过高的 Term 会让原来的 Leader 退位并重新开始选举，这个过程中的损耗明显是没有意义且可避免的。这种情况叫做惊群效应。
此外，有时某些特别的网络分区状况也会因 Term 的不断自增发生更严重的问题，例如：
集群中有 ${a, b, c, d}$ 四个节点，其中 $a$ 为 Leader，节点 $d$ 无法与 ${a, b}$ 通信但与 ${c}$ 的通信正常。这种情况下，由于 $d$ 无法收到 Leader 的日志，永远不可能当选， 会自增 Term。此时 $c$ 节点会跟随 $d$ 更新自身的 Term，从而又导致 $a$ 节点下台，整个集群受 $d$ 的影响无法固定一个 Term 进行正常工作，进而导致系统&lt;strong&gt;完全不可用&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;PreVote&lt;/h2&gt;
&lt;p&gt;为避免上述问题，etcd 和许多生产级 Raft 实现引入了 PreVote（预投票）机制。也就是每次 Candidate 发起选举时，不再自增 Term，而是尝试 Term + 1 进行一次预选举，这次选举不会改变任何节点的参数，避免离群节点的 Term 不断递增；并且，为了防止这个投票过程对正常节点的工作进行干扰，&lt;strong&gt;在至少一个基础选举超时时间范围内&lt;/strong&gt;没有收到有效 Leader 的心跳的节点才能回复投票。如果能通过预选举，才自增 Term 并进行真正选举，缺点就是会拉长正常选主的时间，但由于 Leader 失效本就是低频事件，这一开销在大多数场景下可以接受。&lt;/p&gt;
&lt;h3&gt;活锁&lt;/h3&gt;
&lt;p&gt;prevote 在一些情况下会造成活锁问题。例如：集群中有 ${a, b, c, d}$ 四个节点，节点 $a$ 是原 Leader，但如果节点 $b$, $c$  无法与 $a$ 正常通信，会开始 PreVote 阶段，由于节点 $d$ 仍然能获取到来自 $a$ 的心跳，因此不会为 $b$, $c$ 投票。整个系统进入完全不可用状态。&lt;/p&gt;
&lt;p&gt;针对这个问题，可以引入 check quorum 方法。&lt;/p&gt;
&lt;h2&gt;check quorum&lt;/h2&gt;
&lt;p&gt;如果 Leader 无法从半数节点中获取回复，则 Leader 要主动退位来打破这个僵局，这样保证了 Leader 永远和多数节点相连接，避免 Leader 被网络隔离时出现的问题。&lt;/p&gt;
&lt;h1&gt;何时进行快照？&lt;/h1&gt;
&lt;p&gt;在 6.824 中，我们之间写死了一个阈值，当日志大小超过这个值以后就进行快照。
更好的处理方式是计算快照后的大小，如果远小于当前日志大小，那就说明值得进行快照，但这样需要频繁计算快照大小。所以，我们可以使用上次的快照大小直接对比，当快照大小超出日志大小*扩展因子后进行快照。&lt;/p&gt;
&lt;h1&gt;写入快照太慢？&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;并发进行：把快照的生成放在后台任务中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写时复制：快照过程中冻结当前数据状态，允许主线程继续写入新的状态副本，避免因快照阻塞后续操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Scale-out 有钱加设备！&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Raft 在工程中还有哪些优化？&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Batch：Leader 可以一次收集多个客户端请求，然后批发送给 Follower，降低网络传输开销。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Pipeline：Leader 给 Follower 发送了一批日志之后，它可以直接更新  nextIndex，并且立刻发送后面的日志，不需要等待 Follower 的返回。如果网络出现了错误，或者 Follower 返回错误，Leader 就回滚 nextIndex，然后重新发送日志。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;并行 append：Leader 可以先并行的将日志发送给 Follower的过程和日志落盘的过程可以并行进行，减少落盘开销带来的影响。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Multi-Raft：每台机器可以同时运行多个raft实例，机器之间组建多 Raft 组，客户端请求路由到不同的group上，从而实现多主读写，提高并发性能。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;其他补充&lt;/h1&gt;
&lt;h2&gt;领导权主动转移&lt;/h2&gt;
&lt;p&gt;有些情况下 Leader 需要主动下台或转移给其他节点&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当前 Leader 不在接收请求&lt;/li&gt;
&lt;li&gt;更新目标服务器的日志使其匹配最新日志&lt;/li&gt;
&lt;li&gt;发送请求，让目标服务器先于其他服务器开始选举&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;删除当前 Leader&lt;/h2&gt;
&lt;p&gt;要求 Leader 把自己从集群中删除，一种简单的实现就是上面讲的领导权主动转移方法，转移领导权后再让新 Leader 删除自己。&lt;/p&gt;
&lt;p&gt;另一种方式是直接让 Leader 更改配置删除自己。提交 $C_{new}$ 条目，从配置中删除的领导者将会下台。之后，$C_{new}$ 中的节点进入正常选举流程，选出新的 Leader；不过，如果 Leader 在 $C_{new}$ 被提交前崩溃，有可能在恢复后再次当选 Leader，延迟配置生效。&lt;/p&gt;
&lt;h2&gt;请求路由&lt;/h2&gt;
&lt;p&gt;在 6.824 中，Raft 应用客户端记录下之前访问过的 Leader 节点，每次发送新请求时首先尝试访问这个节点。而首次访问或记录的节点已不再是 Leader（也就是不知道谁是 Leader）的时候，需要随机选择服务器，遍历发起请求找到 Leader。&lt;/p&gt;
&lt;p&gt;这里我们其实可以直接让服务器返回 Leader 的地址，避免客户端再次寻找 Leader 的网络开销。或者让服务端主动路由到 Leader 服务器。&lt;/p&gt;
&lt;h2&gt;高效查询只读命令&lt;/h2&gt;
&lt;p&gt;6.824中，Raft 的读写操作都必须经过完整的日志提交流程，即：客户端请求 → Leader 生成日志 → 复制到多数 → commit → 应用到状态机 → 返回结果。但这显然对只读查询是不必要的浪费 —— 毕竟查询不会修改状态，不需要写入日志。那么，我们是否可以“绕过 Raft 日志”来加速读请求？关键问题是如何保证绕过日志的方式不会破坏线性一致性。&lt;/p&gt;
&lt;h3&gt;ReadIndex&lt;/h3&gt;
&lt;p&gt;为了保证线性一致性，Leader 在处理只读请求前，执行如下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;确保自己是合法 Leader：如果当前 Term 中还没有提交过任何条目，Leader 会先提交一个空操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;记录当前 commitIndex 作为 readIndex。readIndex 代表状态机中&lt;strong&gt;可安全读取&lt;/strong&gt;的最小索引。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发出新一轮的心跳，如果可以获取到多数节点的确认证明没有出现更高 Term 的新 Leader。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;等待状态机应用到 readIndex。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后，Leader 向其状态机发出查询，并向客户端回复结果。&lt;/p&gt;
&lt;h2&gt;进一步优化：基于租约的只读查询&lt;/h2&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上面的方法虽然避免了日志写入，但每批只读请求仍需一次网络交互。为了进一步降低延迟，可以使用租约（Lease）机制：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Leader 在 election timeout 内，自己的 Leader 地位不会被取代，于是可以在这段时间内“直接响应读请求”而无需发送心跳。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这种机制假设跨服务器的时钟漂移没有太大。&lt;/p&gt;
&lt;p&gt;以上！&lt;/p&gt;
&lt;h1&gt;参考资料&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://web.stanford.edu/~ouster/cgi-bin/papers/OngaroPhD.pdf&quot;&gt;Consensus: Bridging Theory and Practice - Diego Ongaro&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/25735592&quot;&gt;TiKV 源码解析系列 - Raft 的优化&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/lizhaolong/p/16437194.html&quot;&gt;聊聊Raft的性能优化&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://steinslab.io/archives/2686&quot;&gt;Raft KV 与 Snapshot&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>go func() 时 runtime 做了些什么？</title><link>https://yomi.moe/posts/go-runtime-func/</link><guid isPermaLink="true">https://yomi.moe/posts/go-runtime-func/</guid><description>PDD 一面</description><pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前几天 PDD 面试被问到这个问题，吟唱 GMP 调度八股，但是很明显面试官很不满意，指出不要背八股，他想听的 runtime 的调度过程。（尽管可能还是因为我八股看的不够多）特查看源码理解一下（，顺带总结一下 GMP 模型。&lt;/p&gt;
&lt;h1&gt;调度模型&lt;/h1&gt;
&lt;p&gt;首先总结一下但凡涉及到 golang 八股就肯定绕不开的 GMP 调度。&lt;/p&gt;
&lt;h2&gt;GM 模型&lt;/h2&gt;
&lt;p&gt;Golang 1.1 前， 使用的是 GM 调度模型。 &lt;code&gt;G&lt;/code&gt; 即 &lt;code&gt;goroutine&lt;/code&gt;，代表 go 协程；&lt;code&gt;M&lt;/code&gt;即&lt;code&gt;Machine&lt;/code&gt;，是处理线程操作的结构体，直接与操作系统进行交互，可以直接视作 OS 线程。新建的 &lt;code&gt;G&lt;/code&gt;会被放到一个全局队列中等待&lt;code&gt;M&lt;/code&gt;的处理。&lt;/p&gt;
&lt;p&gt;来看一下早期 1.0.1 版本的调度部分代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// One round of scheduler: find a goroutine and run it.
// The argument is the goroutine that was running before
// schedule was called, or nil if this is the first call.
// Never returns.
static void
schedule(G *gp)
{
    int32 hz;
    uint32 v;

    schedlock();
    if(gp != nil) {
        // Just finished running gp.
        // 解绑当前的 G 和 M，全局队列的 G 运行数--
        gp-&amp;gt;m = nil;
        runtime·sched.grunning--;

        // atomic { mcpu-- }
        v = runtime·xadd(&amp;amp;runtime·sched.atomic, -1&amp;lt;&amp;lt;mcpuShift);
        if(atomic_mcpu(v) &amp;gt; maxgomaxprocs)
            runtime·throw(&quot;negative mcpu in scheduler&quot;);

        switch(gp-&amp;gt;status){
        case Grunnable:
            // 为什么就绪状态的会被调度？
        case Gdead:
            // Shouldn&apos;t have been running!
            // 唉，你怎么死了
            runtime·throw(&quot;bad gp-&amp;gt;status in sched&quot;);
        case Grunning:
            gp-&amp;gt;status = Grunnable;
            // 回调度队列
            gput(gp);
            break;
        case Gmoribund:
            // 将死的 go 协程，销毁资源
            gp-&amp;gt;status = Gdead; // 埋了
            if(gp-&amp;gt;lockedm) {
                gp-&amp;gt;lockedm = nil;
                m-&amp;gt;lockedg = nil;
            }
            gp-&amp;gt;idlem = nil;
            unwindstack(gp, nil); // 释放栈空间
            gfput(gp); // 回收 G 到空闲列表
            if(--runtime·sched.gcount == 0)
                runtime·exit(0);
            break;
        }
        // 如果 gp 设置了 readyonstop（表示它执行完需要唤醒某个 G）
        if(gp-&amp;gt;readyonstop){
            gp-&amp;gt;readyonstop = 0;
            readylocked(gp);
        }
    } else if(m-&amp;gt;helpgc) { // 清理 GC 辅助协程
        // Bootstrap m or new m started by starttheworld.
        // atomic { mcpu-- }
        v = runtime·xadd(&amp;amp;runtime·sched.atomic, -1&amp;lt;&amp;lt;mcpuShift);
        if(atomic_mcpu(v) &amp;gt; maxgomaxprocs)
            runtime·throw(&quot;negative mcpu in scheduler&quot;);
        // Compensate for increment in starttheworld().
        runtime·sched.grunning--;
        m-&amp;gt;helpgc = 0;
    } else if(m-&amp;gt;nextg != nil) {
        // New m started by matchmg.
        // m 被分配任务
    } else {
        runtime·throw(&quot;invalid m state in scheduler&quot;);
    }

    // Find (or wait for) g to run.  Unlocks runtime·sched.
    // 从队列中找下一个 g 来运行
    gp = nextgandunlock();
    gp-&amp;gt;readyonstop = 0;
    gp-&amp;gt;status = Grunning;
    m-&amp;gt;curg = gp;
    gp-&amp;gt;m = m;

    // Check whether the profiler needs to be turned on or off.
    hz = runtime·sched.profilehz;
    if(m-&amp;gt;profilehz != hz)
        runtime·resetcpuprofiler(hz);

    if(gp-&amp;gt;sched.pc == (byte*)runtime·goexit) {    // kickoff
        runtime·gogocall(&amp;amp;gp-&amp;gt;sched, (void(*)(void))gp-&amp;gt;entry);
    }
    runtime·gogo(&amp;amp;gp-&amp;gt;sched, 0); // 真正切换到 gp 的执行上下文
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先&lt;code&gt;schedlock()&lt;/code&gt;对调度器加锁保护全局队列。然后判断&lt;code&gt;gp&lt;/code&gt;的状态，如果已经完成，则彻底销毁；反之则正常回收当前 goroutine。然后从队列中取下一个 g 来运行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static G*
nextgandunlock(void)
{
    G *gp;
    uint32 v;

top:
    if(atomic_mcpu(runtime·sched.atomic) &amp;gt;= maxgomaxprocs)
        runtime·throw(&quot;negative mcpu&quot;);

    // If there is a g waiting as m-&amp;gt;nextg, the mcpu++
    // happened before it was passed to mnextg.
    if(m-&amp;gt;nextg != nil) {
        gp = m-&amp;gt;nextg;
        m-&amp;gt;nextg = nil;
        schedunlock();

        return gp;
    }
......
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GM 模型的问题？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;从上面的代码可以看出，每次调度都需要获取一遍全局锁，导致频繁的锁竞争。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;M 之间频繁通过&lt;code&gt;G.nextg&lt;/code&gt;传递 g，导致额外开销。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同时每个 M 都有自己的独立内存缓存&lt;code&gt;M.mcache&lt;/code&gt;，造成资源浪费+数据局部性差。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统调用时 M 经常被阻塞或解除阻塞，造成很多额外开销&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;GMP 调度模型&lt;/h2&gt;
&lt;p&gt;GM 模型的问题就是为什么要引入 P 的原因。&lt;code&gt;P&lt;/code&gt; 即为&lt;code&gt;Processor&lt;/code&gt;，表示 &lt;code&gt;M&lt;/code&gt;的执行上下文。每一个 P 都和一个 M 相互绑定。&lt;/p&gt;
&lt;p&gt;每个 P 都会维护自己的本地队列，减轻了对全局队列的依赖，从而减少锁的竞争。同时带来的工作窃取机制也减少了 M 的空转，提高了资源利用率。&lt;/p&gt;
&lt;h3&gt;G, M, P&lt;/h3&gt;
&lt;h3&gt;GMP 调度流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;go func() 创建协程后会加入到一个 P 的本地队列中； 如果本地队列已满则加入全局队列。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个 P 和一个 M 绑定， M 从 P 的本地队列中获取 G 来执行。若 M 绑定的 P 本地队列为空怎么会从其他 P 的本地队列中偷取 G。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若 G 因系统调用阻塞， P 会和当前 M 解绑并寻找新的空闲 M，若没有空闲的 M 则新建一个 M；若 G 因通道或网络 I/O 阻塞，则 M 会寻找其他处于 Grunnable 状态的 G；原来被阻塞的 G 恢复后会重新进入 P 的本地队列等待执行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;G 执行完成后释放资源。&lt;/p&gt;
&lt;h3&gt;schedule()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func schedule() {
    mp := getg().m // 获取当前 G 所在的 M

    ......

top:
    pp := mp.p.ptr() // 获取当前 M 所属的 P
    pp.preempt = false

    ......

    gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available

    ......

    execute(gp, inheritTime)
}# 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过众多版本迭代，1.23 版的 schedule() 实现复杂了很多，其中选择具体 G 的代码封装在 &lt;code&gt;findRunnable()&lt;/code&gt;中，这个函数实在太长，这里只简单讲一下其逻辑。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;findRunnable()&lt;/code&gt; 做的是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;找出一个可执行的 goroutine（gp），并返回是否继承时间片（inheritTime），以及是否唤醒新的 P（tryWakeP）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先尝试调度 traceReader 或 GC worker，这些是 runtime 系统 goroutine，优先级高。然后查看本地队列。如果本地队列是空的，再尝试从全局队列获取 G。为了防止某个 P 的本地队列饱和而其他 P 饿死，每隔一段频率会优先尝试取全局队列的 G。&lt;/p&gt;
&lt;p&gt;如果上面都没有找到任务，并且 spinning M 数量没超过限制，尝试从其他 P 的队列中偷任务。成功偷到就执行；偷不到但发现有新 timer 或 GC 任务则重试。&lt;/p&gt;
&lt;p&gt;最后如果没有任务了，则准备释放 P，阻塞 M。&lt;/p&gt;
&lt;h1&gt;那么 runtime 到底做了什么？&lt;/h1&gt;
&lt;p&gt;编译器会将 go func() { ... } 翻译成 runtime.newproc() 语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Create a new g running fn.
// Put it on the queue of g&apos;s waiting to run.
// The compiler turns a go statement into a call to this.
func newproc(fn *funcval) {
    gp := getg()
    // 获取当前的程序计数器（返回地址），用于调试和分析栈踪迹时追踪调用来源
    pc := sys.GetCallerPC()
    // 切换到当前 M 的系统栈
    systemstack(func() {
        newg := newproc1(fn, gp, pc, false, waitReasonZero)

        // 获取当前线程的 P 并将新创建的 newg 加入到本地队列
        pp := getg().m.p.ptr()
        runqput(pp, newg, true)

        if mainStarted {
            wakep()
        }
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再来看一下 newproc1 的内部逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Create a new g in state _Grunnable (or _Gwaiting if parked is true), starting at fn.
// callerpc is the address of the go statement that created this. The caller is responsible
// for adding the new g to the scheduler. If parked is true, waitreason must be non-zero.
func newproc1(fn *funcval, callergp *g, callerpc uintptr, parked bool, waitreason waitReason) *g {
    if fn == nil {
        fatal(&quot;go of nil func value&quot;)
    }

    mp := acquirem() // disable preemption because we hold M and P in local vars.
    pp := mp.p.ptr()
    newg := gfget(pp)
    if newg == nil {
        newg = malg(stackMin)
        casgstatus(newg, _Gidle, _Gdead)
        allgadd(newg) // publishes with a g-&amp;gt;status of Gdead so GC scanner doesn&apos;t look at uninitialized stack.
    }

    ......

    totalSize := uintptr(4*goarch.PtrSize + sys.MinFrameSize) // extra space in case of reads slightly beyond frame
    totalSize = alignUp(totalSize, sys.StackAlign)
    sp := newg.stack.hi - totalSize

    ......

    memclrNoHeapPointers(unsafe.Pointer(&amp;amp;newg.sched), unsafe.Sizeof(newg.sched))
    newg.sched.sp = sp
    newg.stktopsp = sp
    newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
    newg.sched.g = guintptr(unsafe.Pointer(newg))
    gostartcallfn(&amp;amp;newg.sched, fn)
    newg.parentGoid = callergp.goid
    newg.gopc = callerpc
    newg.ancestors = saveAncestors(callergp)
    newg.startpc = fn.fn

    ...... // 判断是否系统 G，进行标记

    // Track initial transition?
    newg.trackingSeq = uint8(cheaprand())
    if newg.trackingSeq%gTrackingPeriod == 0 {
        newg.tracking = true
    }
    gcController.addScannableStack(pp, int64(newg.stack.hi-newg.stack.lo))

    .......// trace 跟踪

    // Set up race context.
    if raceenabled {
        newg.racectx = racegostart(callerpc)
        newg.raceignore = 0
        if newg.labels != nil {
            // See note in proflabel.go on labelSync&apos;s role in synchronizing
            // with the reads in the signal handler.
            racereleasemergeg(newg, unsafe.Pointer(&amp;amp;labelSync))
        }
    }
    releasem(mp) // 释放锁

    return newg
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在获取到当前 M 后，首先尝试从当前 P 的空闲 G 池中复用，若没有空闲 G，则分配新栈。之后初始化新 G 的调度上下文和元数据。最后返回这个 G。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;最后总结一下标题的答案：&lt;/p&gt;
&lt;p&gt;当使用 go 关键字新建一个 goroutine 时， runtime 会调用 newproc 生成新的 G。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 newproc 中，&lt;strong&gt;&lt;strong&gt;用 systemstack() 切换到 (g0 的) 系统栈&lt;/strong&gt;&lt;/strong&gt;，调用 newproc1。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 newproc1 中，&lt;strong&gt;&lt;strong&gt;首先尝试复用 P 中空闲的 G，若没有，则新建一个 G 实例并为其分配栈空间&lt;/strong&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化调度上下文 (sched)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分配唯一的 goid，记录父 G 的信息、调用栈、标签等元数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置 G 的初始状态为 _Grunnable，即可运行状态。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将这个 G 加入当前 P 的本地队列，让调度器 schedule() 后续执行它。（若已满则进入全局队列）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此外， runtime还会做一些tracing、GC 标记、新栈注册、race 检测等附加工作。&lt;/p&gt;
&lt;h1&gt;附录&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;type p struct {
    id          int32
    status      uint32 // one of pidle/prunning/...
    link        puintptr
    schedtick   uint32     // incremented on every scheduler call
    syscalltick uint32     // incremented on every system call
    sysmontick  sysmontick // last tick observed by sysmon
    m           muintptr   // back-link to associated m (nil if idle)
    mcache      *mcache
    pcache      pageCache
    raceprocctx uintptr

    deferpool    []*_defer // pool of available defer structs (see panic.go)
    deferpoolbuf [32]*_defer

    // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
    goidcache    uint64
    goidcacheend uint64

    // Queue of runnable goroutines. Accessed without lock.
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr
    // runnext, if non-nil, is a runnable G that was ready&apos;d by
    // the current G and should be run next instead of what&apos;s in
    // runq if there&apos;s time remaining in the running G&apos;s time
    // slice. It will inherit the time left in the current time
    // slice. If a set of goroutines is locked in a
    // communicate-and-wait pattern, this schedules that set as a
    // unit and eliminates the (potentially large) scheduling
    // latency that otherwise arises from adding the ready&apos;d
    // goroutines to the end of the run queue.
    //
    // Note that while other P&apos;s may atomically CAS this to zero,
    // only the owner P can CAS it to a valid G.
    runnext guintptr

    // Available G&apos;s (status == Gdead)
    gFree struct {
        gList
        n int32
    }

    sudogcache []*sudog
    sudogbuf   [128]*sudog

    // Cache of mspan objects from the heap.
    mspancache struct {
        // We need an explicit length here because this field is used
        // in allocation codepaths where write barriers are not allowed,
        // and eliminating the write barrier/keeping it eliminated from
        // slice updates is tricky, more so than just managing the length
        // ourselves.
        len int
        buf [128]*mspan
    }

    // Cache of a single pinner object to reduce allocations from repeated
    // pinner creation.
    pinnerCache *pinner

    trace pTraceState

    palloc persistentAlloc // per-P to avoid mutex

    // Per-P GC state
    gcAssistTime         int64 // Nanoseconds in assistAlloc
    gcFractionalMarkTime int64 // Nanoseconds in fractional mark worker (atomic)

    // limiterEvent tracks events for the GC CPU limiter.
    limiterEvent limiterEvent

    // gcMarkWorkerMode is the mode for the next mark worker to run in.
    // That is, this is used to communicate with the worker goroutine
    // selected for immediate execution by
    // gcController.findRunnableGCWorker. When scheduling other goroutines,
    // this field must be set to gcMarkWorkerNotWorker.
    gcMarkWorkerMode gcMarkWorkerMode
    // gcMarkWorkerStartTime is the nanotime() at which the most recent
    // mark worker started.
    gcMarkWorkerStartTime int64

    // gcw is this P&apos;s GC work buffer cache. The work buffer is
    // filled by write barriers, drained by mutator assists, and
    // disposed on certain GC state transitions.
    gcw gcWork

    // wbBuf is this P&apos;s GC write barrier buffer.
    //
    // TODO: Consider caching this in the running G.
    wbBuf wbBuf

    runSafePointFn uint32 // if 1, run sched.safePointFn at next safe point

    // statsSeq is a counter indicating whether this P is currently
    // writing any stats. Its value is even when not, odd when it is.
    statsSeq atomic.Uint32

    // Timer heap.
    timers timers

    // maxStackScanDelta accumulates the amount of stack space held by
    // live goroutines (i.e. those eligible for stack scanning).
    // Flushed to gcController.maxStackScan once maxStackScanSlack
    // or -maxStackScanSlack is reached.
    maxStackScanDelta int64

    // gc-time statistics about current goroutines
    // Note that this differs from maxStackScan in that this
    // accumulates the actual stack observed to be used at GC time (hi - sp),
    // not an instantaneous measure of the total stack size that might need
    // to be scanned (hi - lo).
    scannedStackSize uint64 // stack size of goroutines scanned by this P
    scannedStacks    uint64 // number of goroutines scanned by this P

    // preempt is set to indicate that this P should be enter the
    // scheduler ASAP (regardless of what G is running on it).
    preempt bool

    // gcStopTime is the nanotime timestamp that this P last entered _Pgcstop.
    gcStopTime int64

    // Padding is no longer needed. False sharing is now not a worry because p is large enough
    // that its size class is an integer multiple of the cache line size (for any of our architectures).
}

type g struct {
    // Stack parameters.
    // stack describes the actual stack memory: [stack.lo, stack.hi).
    // stackguard0 is the stack pointer compared in the Go stack growth prologue.
    // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
    // stackguard1 is the stack pointer compared in the //go:systemstack stack growth prologue.
    // It is stack.lo+StackGuard on g0 and gsignal stacks.
    // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
    stack       stack   // offset known to runtime/cgo
    stackguard0 uintptr // offset known to liblink
    stackguard1 uintptr // offset known to liblink

    _panic    *_panic // innermost panic - offset known to liblink
    _defer    *_defer // innermost defer
    m         *m      // current m; offset known to arm liblink
    sched     gobuf
    syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc
    syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc
    syscallbp uintptr // if status==Gsyscall, syscallbp = sched.bp to use in fpTraceback
    stktopsp  uintptr // expected sp at top of stack, to check in traceback
    // param is a generic pointer parameter field used to pass
    // values in particular contexts where other storage for the
    // parameter would be difficult to find. It is currently used
    // in four ways:
    // 1. When a channel operation wakes up a blocked goroutine, it sets param to
    //    point to the sudog of the completed blocking operation.
    // 2. By gcAssistAlloc1 to signal back to its caller that the goroutine completed
    //    the GC cycle. It is unsafe to do so in any other way, because the goroutine&apos;s
    //    stack may have moved in the meantime.
    // 3. By debugCallWrap to pass parameters to a new goroutine because allocating a
    //    closure in the runtime is forbidden.
    // 4. When a panic is recovered and control returns to the respective frame,
    //    param may point to a savedOpenDeferState.
    param        unsafe.Pointer
    atomicstatus atomic.Uint32
    stackLock    uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
    goid         uint64
    schedlink    guintptr
    waitsince    int64      // approx time when the g become blocked
    waitreason   waitReason // if status==Gwaiting

    preempt       bool // preemption signal, duplicates stackguard0 = stackpreempt
    preemptStop   bool // transition to _Gpreempted on preemption; otherwise, just deschedule
    preemptShrink bool // shrink stack at synchronous safe point

    // asyncSafePoint is set if g is stopped at an asynchronous
    // safe point. This means there are frames on the stack
    // without precise pointer information.
    asyncSafePoint bool

    paniconfault bool // panic (instead of crash) on unexpected fault address
    gcscandone   bool // g has scanned stack; protected by _Gscan bit in status
    throwsplit   bool // must not split stack
    // activeStackChans indicates that there are unlocked channels
    // pointing into this goroutine&apos;s stack. If true, stack
    // copying needs to acquire channel locks to protect these
    // areas of the stack.
    activeStackChans bool
    // parkingOnChan indicates that the goroutine is about to
    // park on a chansend or chanrecv. Used to signal an unsafe point
    // for stack shrinking.
    parkingOnChan atomic.Bool
    // inMarkAssist indicates whether the goroutine is in mark assist.
    // Used by the execution tracer.
    inMarkAssist bool
    coroexit     bool // argument to coroswitch_m

    raceignore    int8  // ignore race detection events
    nocgocallback bool  // whether disable callback from C
    tracking      bool  // whether we&apos;re tracking this G for sched latency statistics
    trackingSeq   uint8 // used to decide whether to track this G
    trackingStamp int64 // timestamp of when the G last started being tracked
    runnableTime  int64 // the amount of time spent runnable, cleared when running, only used when tracking
    lockedm       muintptr
    fipsIndicator uint8
    sig           uint32
    writebuf      []byte
    sigcode0      uintptr
    sigcode1      uintptr
    sigpc         uintptr
    parentGoid    uint64          // goid of goroutine that created this goroutine
    gopc          uintptr         // pc of go statement that created this goroutine
    ancestors     *[]ancestorInfo // ancestor information goroutine(s) that created this goroutine (only used if debug.tracebackancestors)
    startpc       uintptr         // pc of goroutine function
    racectx       uintptr
    waiting       *sudog         // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
    cgoCtxt       []uintptr      // cgo traceback context
    labels        unsafe.Pointer // profiler labels
    timer         *timer         // cached timer for time.Sleep
    sleepWhen     int64          // when to sleep until
    selectDone    atomic.Uint32  // are we participating in a select and did someone win the race?

    // goroutineProfiled indicates the status of this goroutine&apos;s stack for the
    // current in-progress goroutine profile
    goroutineProfiled goroutineProfileStateHolder

    coroarg   *coro // argument during coroutine transfers
    syncGroup *synctestGroup

    // Per-G tracer state.
    trace gTraceState

    // Per-G GC state

    // gcAssistBytes is this G&apos;s GC assist credit in terms of
    // bytes allocated. If this is positive, then the G has credit
    // to allocate gcAssistBytes bytes without assisting. If this
    // is negative, then the G must correct this by performing
    // scan work. We track this in bytes to make it fast to update
    // and check for debt in the malloc hot path. The assist ratio
    // determines how this corresponds to scan work debt.
    gcAssistBytes int64
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>2024 年终总结</title><link>https://yomi.moe/posts/2024/</link><guid isPermaLink="true">https://yomi.moe/posts/2024/</guid><pubDate>Tue, 31 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;又到年底了，不能再和去年一样一咕再咕了（&lt;/p&gt;
&lt;h1&gt;学业&lt;/h1&gt;
&lt;p&gt;24fall 申研的失利让我 emo 了挺久，最后还是决定 gap 半年准备 25spring 了。&lt;/p&gt;
&lt;p&gt;今年六月顺利毕业了，但因为忘记申请英文的毕业证书和学位证书又跑回学校一趟。最后又逛了一圈软件园和主校区。从软件园的六区向下俯视，感慨万千，也有了真正毕业的实感。&lt;s&gt;我可能一辈子也忘不了沙袋吧&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;在第一个博客写下的大学目标，虽然基本实现，但回过头来看感觉充斥着对大学和社会的不切实际的幻想，也没有太多收获。下一步尽量在目标实现过程中多感受体会，希望最后结果是自我提升而非自我陶醉。&lt;/p&gt;
&lt;p&gt;最后 25spring 开奖结果也不怎么样，但好在拿到了学校的奖学金，也说不上是很差的结果吧。&lt;/p&gt;
&lt;h1&gt;社交&lt;/h1&gt;
&lt;p&gt;我不算是很会社交的人，所以真的很感激主动来和我交好的朋友们。今年虽然也没什么新朋友但是和老朋友的友谊加深不也是一种进步吗（&lt;/p&gt;
&lt;p&gt;争取明年自己也能多迈出几步，扩展一下人脉了。&lt;/p&gt;
&lt;h1&gt;闲杂&lt;/h1&gt;
&lt;h2&gt;ACG&lt;/h2&gt;
&lt;p&gt;游戏方面今年大部分时间都在陪朋友打猎，很可惜我这个配置明年荒野应该是不能第一时间体验了。今年玩过的几个新游戏里最喜欢的应该是碧蓝幻想relink，只可惜 cy 不当人。当然，黑神话也挺让我满意的（&lt;/p&gt;
&lt;p&gt;杀戮尖塔也在今年全角色 a20 通关并全成就了，不得不说真的是卡牌 rouge 中的 top1，完美的设计。&lt;/p&gt;
&lt;p&gt;退坑了 ba，nexon 和悠星真没活了。&lt;/p&gt;
&lt;p&gt;今年一共就玩了三款文字 avg，十分幸运能在一年的末尾玩到播种之谣这种氛围极佳的催泪系作品，给我的体验不亚于 Clannad，有时间也许我会再写一篇博文评论一次这个游戏吧。&lt;/p&gt;
&lt;h2&gt;音乐&lt;/h2&gt;
&lt;p&gt;网易云年终总结保持了五年记录的歌手终于换人了（&lt;/p&gt;
&lt;p&gt;在 gbc 播出的那段日子每天高强度循环刺团的歌，看到这个结果也没什么意外（&lt;/p&gt;
&lt;p&gt;好想去看一次刺刺的 live 啊（&lt;/p&gt;
&lt;h1&gt;2025&lt;/h1&gt;
&lt;p&gt;总之，先向前吧。&lt;/p&gt;
&lt;p&gt;新一年的目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;彻底不看国内平台的奶头乐和简中互联网的垃圾内容&lt;/li&gt;
&lt;li&gt;再多结交一些志同道合的朋友&lt;/li&gt;
&lt;li&gt;多做一些有价值的开源&lt;/li&gt;
&lt;li&gt;找个大厂实习（&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>MIT 6.5840（原 6.824) 通关笔记 Lab5 Sharded Key/Value Service</title><link>https://yomi.moe/posts/mit65840-5/</link><guid isPermaLink="true">https://yomi.moe/posts/mit65840-5/</guid><pubDate>Fri, 20 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最后的 Lab 要求我们实现一个分片 k/v 存储系统。&lt;/p&gt;
&lt;h1&gt;Part A: The Controller and Static Sharding&lt;/h1&gt;
&lt;p&gt;大概的框架直接照抄 lab4 就可以，麻烦的点在于如何平均分配切片并尽可能减少移动。&lt;/p&gt;
&lt;p&gt;注意 golang 的 map 存储的是引用，所以新建 configuration 时要新建一个 map。&lt;/p&gt;
&lt;p&gt;另外 golang 的 map 也是哈希的，所以 for k, v := range 遍历 map 会导致各节点的分片不同，要保证每次遍历 map 都是按照一定顺序的。&lt;/p&gt;
&lt;h2&gt;切片的分配&lt;/h2&gt;
&lt;p&gt;首先分析一下：平均分配意味着每组会分配到 shardNum := len(conf.Shards) / len(conf.Groups) 个分片，而其中 toleranceNum := len(conf.Shards) % len(conf.Groups) 个组会多分配到一个分片。&lt;/p&gt;
&lt;p&gt;而为了尽可能减少分片的移动，可以将分片数大于 shardNum 的节点多出来的分片以及 0 的分片标记为 &lt;code&gt;待移出&lt;/code&gt;，而分片数少于 shardNum 的节点则标记缺少量个数的&lt;code&gt;待补充&lt;/code&gt;。当 toleranceNum &amp;gt; 0 时可以“容忍” toleranceNum 个节点多一个分片。&lt;/p&gt;
&lt;p&gt;标记完成后如果容忍的节点数仍小于 toleranceNum，则让差值数量的&lt;code&gt;待补充&lt;/code&gt;节点额外承担切片。&lt;/p&gt;
&lt;p&gt;最后将 &lt;code&gt;待移出&lt;/code&gt;的分片分配到&lt;code&gt;待补充&lt;/code&gt;节点即可。&lt;/p&gt;
&lt;p&gt;代码实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func reorder(conf *Config) {
    if len(conf.Groups) == 0 {
        return
    }
    shardNum := len(conf.Shards) / len(conf.Groups)
    remainNum := len(conf.Shards) % len(conf.Groups)
    shardSingal := make(map[int]int)
    gidRemove := make([]int, 0, 5)
    gidAdd := make([]int, 0, 5)
    for key := range conf.Groups {
        shardSingal[key] = shardNum
    }
    shardSingal[0] = 0
    toleranceNum := remainNum
    for _, val := range conf.Shards {
        if _, ok := shardSingal[val]; ok {
            shardSingal[val]--
        }
    }
    keysSorted := make([]int, 0, len(shardSingal))
    for key := range shardSingal {
        keysSorted = append(keysSorted, key)
    }
    sort.Ints(keysSorted)
    for _, key := range keysSorted {
        for shardSingal[key] &amp;lt; 0 {
            if shardSingal[key] == -1 &amp;amp;&amp;amp; toleranceNum &amp;gt; 0 &amp;amp;&amp;amp; key != 0 {
                toleranceNum--
                break
            }
            gidRemove = append(gidRemove, key)
            shardSingal[key]++
        }
        for shardSingal[key] &amp;gt; 0 {
            gidAdd = append(gidAdd, key)
            shardSingal[key]--
        }
    }
    if shardNum == 0 &amp;amp;&amp;amp; toleranceNum &amp;gt; 0 {
        for _, key := range keysSorted {
            if toleranceNum &amp;lt;= 0 {
                break
            }
            if key != 0 &amp;amp;&amp;amp; shardSingal[key] == 0 {
                gidAdd = append(gidAdd, key)
                toleranceNum--
            }
        }
    }
    end := len(gidAdd)
    for ptr := 0; ptr &amp;lt; end &amp;amp;&amp;amp; toleranceNum &amp;gt; 0; ptr++ {
        if ptr+1 &amp;lt; end &amp;amp;&amp;amp; gidAdd[ptr] == gidAdd[ptr+1] {
            continue
        }
        gidAdd = append(gidAdd, gidAdd[ptr])
        toleranceNum--
    }
    ptr := 0
    for i := 0; i &amp;lt; len(conf.Shards) &amp;amp;&amp;amp; ptr &amp;lt; len(gidRemove); i++ {
        if conf.Shards[i] == gidRemove[ptr] {
            conf.Shards[i] = gidAdd[ptr]
            ptr++
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;具体实现&lt;/h2&gt;
&lt;p&gt;都做到这里了实现这些东西肯定是很轻松的，没什么好讲的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (sc *ShardCtrler) apply() {
    for {
        applyMsg := &amp;lt;-sc.applyCh
        if applyMsg.CommandValid {
            op, _ := applyMsg.Command.(Op)
            sc.mu.Lock()
            if _, ok := sc.history[op.ID]; ok {
                sc.mu.Unlock()
                continue
            }
            switch op.Operation {
            case &quot;Join&quot;:
                lastConfig := sc.configs[len(sc.configs)-1]
                lastConfig.Groups = map[int][]string{}
                for k, v := range sc.configs[len(sc.configs)-1].Groups {
                    lastConfig.Groups[k] = v
                }
                for k, v := range op.Servers {
                    lastConfig.Groups[k] = v
                }
                lastConfig.Num = len(sc.configs)
                reorder(&amp;amp;lastConfig)
                sc.configs = append(sc.configs, lastConfig)
                sc.lastApplied = &quot;Join&quot;
                sc.history[op.ID] = &quot;1&quot;
            case &quot;Leave&quot;:
                lastConfig := sc.configs[len(sc.configs)-1]
                lastConfig.Num = len(sc.configs)
                lastConfig.Groups = map[int][]string{}
                for k, v := range sc.configs[len(sc.configs)-1].Groups {
                    lastConfig.Groups[k] = v
                }
                for _, val := range op.GIDs {
                    for i, v := range lastConfig.Shards {
                        if v == val {
                            lastConfig.Shards[i] = 0
                        }
                    }
                    delete(lastConfig.Groups, val)
                }
                reorder(&amp;amp;lastConfig)
                sc.configs = append(sc.configs, lastConfig)
                sc.lastApplied = &quot;Leave&quot;
                sc.history[op.ID] = &quot;1&quot;
            case &quot;Move&quot;:
                if !(sc.lastApplied == &quot;Join&quot; || sc.lastApplied == &quot;Leave&quot;) {
                    sc.lastApplied = &quot;Move&quot;
                    lastConfig := sc.configs[len(sc.configs)-1]
                    lastConfig.Shards[op.Shard] = op.GID
                    lastConfig.Num = len(sc.configs)
                    sc.configs = append(sc.configs, lastConfig)
                }
                sc.history[op.ID] = &quot;1&quot;
            case &quot;Query&quot;:
                sc.lastApplied = &quot;Query&quot;
                sc.history[op.ID] = &quot;1&quot;
            }
            delete(sc.history, op.LastOpID)
            sc.mu.Unlock()
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Part B: Shard Movement&lt;/h1&gt;
&lt;p&gt;首先将 lab4 的代码 copy 过来，就可以 pass 第一个 test。&lt;/p&gt;
&lt;p&gt;在每次接收请求后判断分片是否正确，不正确则直接返回，实现后即可 pass 第二个 test。&lt;/p&gt;
&lt;h2&gt;分片的迁移&lt;/h2&gt;
&lt;p&gt;采用向之前持有分片的组发送 RPC 请求。&lt;/p&gt;
&lt;p&gt;获取分片时肯定是向每组的 leader 发送，但如果组内的每个 server 都发送 RPC 的话比较复杂，并且不好处理 challenge1 中的删除分片。思考了一下，应当让每组中只有 leader 不断检测，其他节点的作用单纯是在分布式系统保持数据一致性。&lt;/p&gt;
&lt;p&gt;起一个协程不断查询是否有更新的 configuration，如果有更新且&lt;code&gt;待接收的分片&lt;/code&gt;为空时将其写入 raft 通知组内所有节点更新配置。应用新的配置时找出&lt;code&gt;需要发送的分片&lt;/code&gt;和&lt;code&gt;待接收的分片&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (kv *ShardKV) poll() {
    _, isLeader := kv.rf.GetState()
    kv.mu.Lock()
    if !isLeader || len(kv.newShard) &amp;gt; 0 {
        kv.mu.Unlock()
        return
    }
    next := kv.conf.Num + 1
    kv.mu.Unlock()
    conf := kv.mck.Query(next)
    if conf.Num == next {
        kv.rf.Start(conf)
    }
}

func (kv *ShardKV) updateDataShard(conf shardctrler.Config) {
    kv.mu.Lock()
    defer kv.mu.Unlock()
    if conf.Num &amp;lt;= kv.conf.Num {
        return
    }
    lastConf, outShard := kv.conf, kv.authFlags
    kv.authFlags, kv.conf = make(map[int]bool), conf
    for shard, gid := range conf.Shards {
        if gid != kv.gid {
            continue
        }
        if _, ok := outShard[shard]; ok || lastConf.Num == 0 {
            kv.authFlags[shard] = true
            delete(outShard, shard)
        } else {
            kv.newShard[shard] = lastConf.Num
        }
    }
    if len(outShard) &amp;gt; 0 {
        kv.outShards[lastConf.Num] = make(map[int]map[string]string)
        for shard := range outShard {
            targetPairs := make(map[string]string)
            for k, v := range kv.pairs {
                if key2shard(k) == shard {
                    targetPairs[k] = v
                }
            }
            kv.outShards[lastConf.Num][shard] = targetPairs
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再起一个协程不断检测是否有&lt;code&gt;待接收的分片&lt;/code&gt;，若有则向对应组发送获取分片请求。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (kv *ShardKV) getDataShard() {
    for !kv.killed() {
        _, isLeader := kv.rf.GetState()
        kv.mu.Lock()
        if !isLeader || len(kv.newShard) == 0 {
            kv.mu.Unlock()
            time.Sleep(50 * time.Millisecond)
            continue
        }
        var wg sync.WaitGroup
        for shard, num := range kv.newShard {
            wg.Add(1)
            go func(shard int, conf shardctrler.Config) {
                kv.sendShardRequest(shard, conf)
                wg.Done()
            }(shard, kv.mck.Query(num))
        }
        kv.mu.Unlock()
        wg.Wait()
        time.Sleep(50 * time.Millisecond)
    }
}

func (kv *ShardKV) sendShardRequest(shard int, conf shardctrler.Config) {
    args := ShardArgs{Shard: shard, Num: conf.Num}
    for _, srv := range conf.Groups[conf.Shards[shard]] {
        srv := kv.make_end(srv)
        reply := ShardReply{}
        ok := srv.Call(&quot;ShardKV.ReceiveShardRequest&quot;, &amp;amp;args, &amp;amp;reply)
        if ok &amp;amp;&amp;amp; reply.Err == OK {
            kv.rf.Start(ShardMove{Pairs: reply.AppendPairs, Sequence: reply.Sequence, Shard: shard, Num: conf.Num})
            return
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功后将新分片数据写入 raft 共享给全组。&lt;/p&gt;
&lt;h2&gt;快照&lt;/h2&gt;
&lt;p&gt;因为要保证每次重启从上次快照点重新应用日志能够完全还原系统状态，像是待移出的切片等数据也要保存在快照中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (kv *ShardKV) MakeSnapshot(persister *raft.Persister, maxraftstate int) {
    for !kv.killed() {
        kv.mu.Lock()
        kv.cond.Wait()
        if persister.RaftStateSize() &amp;gt; maxraftstate*4/5 {
            w := new(bytes.Buffer)
            e := labgob.NewEncoder(w)
            e.Encode(kv.pairs)
            e.Encode(kv.sequence)
            e.Encode(kv.newShard)
            e.Encode(kv.outShards)
            e.Encode(kv.authFlags)
            e.Encode(kv.conf)
            e.Encode(kv.garbages)
            snapshot := w.Bytes()
            kv.rf.Snapshot(kv.lastIndex, snapshot)
        }
        kv.mu.Unlock()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;去重&lt;/h2&gt;
&lt;p&gt;因为之前一直采用存储操作历史记录的方式来去重，而分片迁移时无法特定对应切片的历史记录。所以需要更改历史记录为&lt;code&gt;history   map[int]map[int64]Result&lt;/code&gt;，分切片存储历史记录。&lt;/p&gt;
&lt;p&gt;在进行重复测试时，发现 TestConcurrent3_5B 中，有可能出现已经完成的指令在分片迁移之后才进行 RPC 的回复，这会导致 Get 无法及时回复有效的结果。&lt;/p&gt;
&lt;p&gt;所有最后还是用通道回复结果（&lt;/p&gt;
&lt;p&gt;而且 go 从设计就能看出是奉行通过通信来共享内存，而不是通过共享内存来通信的。&lt;/p&gt;
&lt;p&gt;这里参考了简书上的一篇博文&lt;a href=&quot;https://www.jianshu.com/p/f5c8ab9cd577&quot;&gt;^1&lt;/a&gt;，为每个客户端分配 id 而非为操作分配 id，并让每个客户端记下每次请求的序号。server 侧存储每个客户端的最新已进行操作序号，序号小于记录的操作不再进行，以此达到去重。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Clerk struct {
    sm       *shardctrler.Clerk
    config   shardctrler.Config
    make_end func(string) *labrpc.ClientEnd
    // You will have to modify this struct.
    id  int64
    num int
}

func MakeClerk(ctrlers []*labrpc.ClientEnd, make_end func(string) *labrpc.ClientEnd) *Clerk {
    ck := new(Clerk)
    ck.sm = shardctrler.MakeClerk(ctrlers)
    ck.make_end = make_end
    // You&apos;ll have to add code here.
    ck.id = nrand()
    ck.num = 0
    return ck
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Challenge 1&lt;/h1&gt;
&lt;p&gt;Challenge 1 要求实现对无用内存的释放。&lt;/p&gt;
&lt;p&gt;首先要明确我们可以删除什么样的数据。当一个节点向另一组发送分片后，如果能够确保该分片已被接受，则本地节点的分片数据可以不再保留。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (kv *ShardKV) applyShardMove(data ShardMove) {
    kv.mu.Lock()
    defer kv.mu.Unlock()
    if data.Num != kv.conf.Num-1 {
        return
    }
    delete(kv.newShard, data.Shard)
    if _, ok := kv.authFlags[data.Shard]; !ok {
        kv.authFlags[data.Shard] = true
        for k, v := range data.Pairs {
            kv.pairs[k] = v
        }
        for k, v := range data.Sequence {
            kv.sequence[k] = max(v, kv.sequence[k])
        }
        if _, ok := kv.garbages[data.Num]; !ok {
            kv.garbages[data.Num] = make(map[int]bool)
        }
        kv.garbages[data.Num][data.Shard] = true
    }
    kv.cond.Broadcast()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说要采取二阶段提交来确保正确的垃圾回收。&lt;/p&gt;
&lt;p&gt;另起协程不断检测是否有垃圾需要进行处理，若有则向对应组发送 RPC 通知对方可以清除。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (kv *ShardKV) requestGarbageCollection() {
    _, isLeader := kv.rf.GetState()
    kv.mu.Lock()
    if !isLeader || len(kv.garbages) == 0 {
        kv.mu.Unlock()
        return
    }
    var wg sync.WaitGroup
    for num, shards := range kv.garbages {
        for shard := range shards {
            wg.Add(1)
            go func(shard int, cfg shardctrler.Config) {
                defer wg.Done()
                args := ShardArgs{Shard: shard, Num: cfg.Num}
                gid := cfg.Shards[shard]
                for _, server := range cfg.Groups[gid] {
                    srv := kv.make_end(server)
                    reply := ShardReply{}
                    if ok := srv.Call(&quot;ShardKV.GarbageCollection&quot;, &amp;amp;args, &amp;amp;reply); ok &amp;amp;&amp;amp; reply.Err == OK {
                        kv.mu.Lock()
                        defer kv.mu.Unlock()
                        delete(kv.garbages[cfg.Num], shard)
                        if len(kv.garbages[cfg.Num]) == 0 {
                            delete(kv.garbages, cfg.Num)
                        }
                    }
                }
            }(shard, kv.mck.Query(num))
        }
    }
    kv.mu.Unlock()
    wg.Wait()
}


func (kv *ShardKV) GarbageCollection(args *ShardArgs, reply *ShardReply) {
    reply.Err = ErrWrongLeader
    kv.mu.Lock()
    if _, ok := kv.outShards[args.Num]; !ok {
        kv.mu.Unlock()
        return
    }
    if _, ok := kv.outShards[args.Num][args.Shard]; !ok {
        kv.mu.Unlock()
        return
    }
    gc := GC{CfgNum: args.Num, Shard: args.Shard}
    kv.mu.Unlock()
    index, _, isLeader := kv.rf.Start(gc)
    if isLeader {
        ch := kv.getNotifyCh(index, true)
        msg := kv.getNotified(index, ch)
        if msg.success {
            reply.Err = OK
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Challenge 2&lt;/h1&gt;
&lt;p&gt;按照上面的实现，Challenge 2 可以直接 pass。&lt;/p&gt;
&lt;p&gt;configuration 的依次更新保证了即便在配置更新过程中系统也能正常运行，而采用区分分片的处理权限状态就可以在配置完全更新前保证已完成分片的处理。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;历经千辛万苦，终于完成了所有 Lab。&lt;/p&gt;
&lt;p&gt;虽然最终还是参考了别人的解决方案完成了 Lab 5B，但这段极度折磨的经历真的可以让人深刻体会到分布式系统的复杂性，前几天每天早上醒来想到还要继续调试 Lab5B 就不想起床了。。。&lt;/p&gt;
&lt;p&gt;个人对所有 Lab 的难度排序是：Lab5B &amp;gt;&amp;gt;&amp;gt; Lab3B &amp;gt;&amp;gt; Lab3其他Part &amp;gt; Lab4 &amp;gt; 其他&lt;/p&gt;
&lt;h1&gt;Footnotes&lt;/h1&gt;
</content:encoded></item><item><title>MIT 6.5840（原 6.824) 通关笔记 Lab4 Fault-tolerant Key/Value Service</title><link>https://yomi.moe/posts/mit65840-4/</link><guid isPermaLink="true">https://yomi.moe/posts/mit65840-4/</guid><description>最解压的一集</description><pubDate>Sat, 07 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这次的 Lab 要求我们使用 Lab3 中实现的 raft 构建一个 kvserver。&lt;/p&gt;
&lt;h1&gt;Part A: Key/value service without snapshots&lt;/h1&gt;
&lt;h2&gt;kvserver&lt;/h2&gt;
&lt;p&gt;Clerk 向记录的 leader kvserver 发送请求，如果超时或失败，则重新寻找 leader 并发送请求。&lt;/p&gt;
&lt;p&gt;kvserver 收到请求后调用 Start()，等待 Raft 完成共识。提交的命令完成后回复 Clerk 的 RPC。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;kvserver 侧如何得知结果被应用？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不妨利用 Lab 2 中用到的历史记录。在每次命令被提交并执行后将 history 设置，RPC 处理部分不断检测对应项，当对应项不为空时则判断为命令执行完成。&lt;/p&gt;
&lt;p&gt;每个新操作的 RPC 请求都会附带上一操作的序号，表示客户端已经收到上次操作的结果。剩下的就交给 Raft 吧。&lt;/p&gt;
&lt;p&gt;完成以后初次测试只有第二个 test case 出现了超时错误&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test_test.go:419: Operations completed too slowly 100.859572ms/op &amp;gt; 33.333333ms/op
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试要求每 100ms 至少进行三次操作，如果按我在 Lab 3 中的固定每 100ms 发送一次 AppendEntries 的做法肯定是过不了的，所以又得回去改 Raft（&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) Start(command interface{}) (int, int, bool) {
	index := -1
	term := -1
	isLeader := true

	// Your code here (3B).
	rf.mu.Lock()
	defer rf.mu.Unlock()
	term, isLeader = rf.CurrentTerm, rf.state == LEADER
	if !isLeader {
		return index, term, isLeader
	} else {
		index = len(rf.Log) + rf.SnapshotState.SnapshotIndex
		rf.Log = append(rf.Log, Log{Command: command, Term: rf.CurrentTerm})
		rf.persist()
		rf.matchIndex[rf.me] = len(rf.Log) - 1 + rf.SnapshotState.SnapshotIndex
		go func() {
			time.Sleep(1 * time.Millisecond)
			rf.mu.Lock()
			if rf.state != LEADER {
				rf.mu.Unlock()
				return
			}
			rf.sendAppendEntriesToAll()
			rf.mu.Unlock()
		}()
	}
	return index, term, isLeader
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;具体实现&lt;/h2&gt;
&lt;h3&gt;client.go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func MakeClerk(servers []*labrpc.ClientEnd) *Clerk {
	ck := new(Clerk)
	ck.servers = servers
	// You&apos;ll have to add code here.
	ck.leader = mathRand.Intn(len(servers))
	ck.lastOpID = 0
	return ck
}
func (ck *Clerk) Get(key string) string {
	// You will have to modify this function.
	opId := nrand()
	args := GetArgs{Key: key, ID: opId, LastOpID: ck.lastOpID}
	start := ck.leader
	for {
		reply := GetReply{}
		resultCh := make(chan bool)
		leader := ck.leader
		var ok bool
		go func() {
			time.Sleep(100 * time.Millisecond)
			resultCh &amp;lt;- false
		}()
		go func() {
			ok = ck.servers[leader].Call(&quot;KVServer.Get&quot;, &amp;amp;args, &amp;amp;reply)
			resultCh &amp;lt;- true
		}()
		if &amp;lt;-resultCh {
			if ok &amp;amp;&amp;amp; reply.Err == &quot;&quot; {
				ck.lastOpID = opId
				return reply.Value
			}
		}
		ck.leader = (ck.leader + 1) % len(ck.servers)
		if ck.leader == start {
			time.Sleep(200 * time.Millisecond)
		}
	}
}
func (ck *Clerk) PutAppend(key string, value string, op string) {
	// You will have to modify this function.
	opId := nrand()
	args := PutAppendArgs{Key: key, Value: value, ID: opId, LastOpID: ck.lastOpID}
	start := ck.leader
	for {
		resultCh := make(chan bool)
		var ok bool
		leader := ck.leader
		reply := PutAppendReply{}
		go func() {
			ok = ck.servers[leader].Call(&quot;KVServer.&quot;+op, &amp;amp;args, &amp;amp;reply)
			resultCh &amp;lt;- true
		}()
		go func() {
			time.Sleep(100 * time.Millisecond)
			resultCh &amp;lt;- false
		}()
		if &amp;lt;-resultCh {
			if ok &amp;amp;&amp;amp; reply.Err == &quot;&quot; {
				ck.lastOpID = opId
				break
			}
		}
		ck.leader = (ck.leader + 1) % len(ck.servers)
		if ck.leader == start {
			time.Sleep(200 * time.Millisecond)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化时随机给 Clerk 分配一个假 leader，当请求失败后再更换节点尝试，全部节点都失败后等待一会儿再开始新一轮的重试。&lt;/p&gt;
&lt;h3&gt;server.go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
	// Your code here.
	op := Op{Operation: &quot;Get&quot;, Key: args.Key, ID: args.ID, LastOpID: args.LastOpID}
	_, _, success := kv.rf.Start(op)
	if !success {
		reply.Err = &quot;not leader&quot;
	} else {
		for !kv.killed() {
			if _, isLeader := kv.rf.GetState(); !isLeader {
				reply.Err = &quot;leader expired&quot;
				return
			}
			kv.mu.Lock()
			if _, ok := kv.history[op.ID]; ok {
				reply.Value = kv.pairs[op.Key]
				kv.mu.Unlock()
				return
			}
			kv.mu.Unlock()
			time.Sleep(1 * time.Millisecond)
		}
	}
}
func (kv *KVServer) apply() {
	for !kv.killed() {
		applyMsg := &amp;lt;-kv.applyCh
		if applyMsg.CommandValid {
			op, _ := applyMsg.Command.(Op)
			kv.mu.Lock()
			if _, ok := kv.history[op.ID]; ok {
				kv.mu.Unlock()
				continue
			}
			delete(kv.history, op.LastOpID)

			switch op.Operation {
			case &quot;Get&quot;:
				kv.history[op.ID] = kv.pairs[op.Key]
			case &quot;Put&quot;:
				kv.pairs[op.Key] = op.Value
				kv.history[op.ID] = &quot;&quot;
			case &quot;Append&quot;:
				kv.pairs[op.Key] += op.Value
				kv.history[op.ID] = &quot;&quot;
			}
			kv.mu.Unlock()
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另起一协程不断接收命令，和 lab2 类似，服务器执行命令前首先检查是否已完成过该命令，避免重复执行。&lt;/p&gt;
&lt;h1&gt;Part B: Key/value service with snapshots&lt;/h1&gt;
&lt;p&gt;Part B 要求我们为 kvserver 加入快照。没太多好说的，感觉难度不应该标 hard。&lt;/p&gt;
&lt;p&gt;为了避免延迟后的 RPC 造成重复的日志应用，删除 history 中的记录时另开一个协程，延迟一段时间再进行删除。&lt;/p&gt;
&lt;p&gt;在重启之后也要避免重复的日志应用，所以需要持久化的有存储状态、应用历史以及已应用索引。&lt;/p&gt;
&lt;h2&gt;具体实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func (kv *KVServer) MakeSnapshot(persister *raft.Persister, maxraftstate int) {
	for !kv.killed() {
		kv.mu.Lock()
		kv.cond.Wait()
		if persister.RaftStateSize() &amp;gt; maxraftstate*4/5 {
			w := new(bytes.Buffer)
			e := labgob.NewEncoder(w)
			e.Encode(kv.pairs)
			e.Encode(kv.history)
			e.Encode(kv.lastIndex)
			snapshot := w.Bytes()
			kv.rf.Snapshot(kv.lastIndex, snapshot)
		}
		kv.mu.Unlock()
	}
}
func (kv *KVServer) apply() {
	for !kv.killed() {
		applyMsg := &amp;lt;-kv.applyCh
		if applyMsg.CommandValid {
			op, _ := applyMsg.Command.(Op)
			kv.mu.Lock()
			kv.lastIndex = applyMsg.CommandIndex
			if _, ok := kv.history[op.ID]; ok {
				kv.mu.Unlock()
				continue
			}
			switch op.Operation {
			case &quot;Get&quot;:
				kv.history[op.ID] = &quot;1&quot;
				kv.history[op.LastOpID] = &quot;2&quot;
			case &quot;Put&quot;:
				kv.pairs[op.Key] = op.Value
				kv.history[op.ID] = &quot;1&quot;
				kv.history[op.LastOpID] = &quot;2&quot;
			case &quot;Append&quot;:
				kv.pairs[op.Key] += op.Value
				kv.history[op.ID] = &quot;1&quot;
				kv.history[op.LastOpID] = &quot;2&quot;
			}

			go func(id int64) {
				time.Sleep(100 * time.Millisecond)
				kv.mu.Lock()
				delete(kv.history, id)
				kv.mu.Unlock()
			}(op.LastOpID)
			kv.cond.Broadcast()
			kv.mu.Unlock()
		} else if applyMsg.SnapshotValid {
			kv.mu.Lock()
			r := bytes.NewBuffer(applyMsg.Snapshot)
			d := labgob.NewDecoder(r)
			d.Decode(&amp;amp;kv.pairs)
			d.Decode(&amp;amp;kv.history)
			d.Decode(&amp;amp;kv.lastIndex)
			for key, value := range kv.history {
				go func(k int64, v string) {
					time.Sleep(100 * time.Millisecond)
					if v == &quot;2&quot; {
						kv.mu.Lock()
						delete(kv.history, k)
						kv.mu.Unlock()
					}
				}(key, value)

			}
			kv.mu.Unlock()
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在初始化函数中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if persister.ReadSnapshot() != nil &amp;amp;&amp;amp; len(persister.ReadSnapshot()) &amp;gt;= 1 {
		r := bytes.NewBuffer(persister.ReadSnapshot())
		d := labgob.NewDecoder(r)
		d.Decode(&amp;amp;kv.pairs)
		d.Decode(&amp;amp;kv.history)
		d.Decode(&amp;amp;kv.lastIndex)
		kv.mu.Lock()
		for key, value := range kv.history {
			go func(k int64, v string) {
				time.Sleep(100 * time.Millisecond)
				if v == &quot;2&quot; {
					kv.mu.Lock()
					delete(kv.history, k)
					kv.mu.Unlock()
				}
			}(key, value)
		}
		kv.mu.Unlock()
	}
	if maxraftstate &amp;gt;= 0 {
		go kv.MakeSnapshot(persister, maxraftstate)
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;单次测试结果&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;=== RUN   TestBasic4A
Test: one client (4A) ...
  ... Passed --  15.1  5 10461 1973
--- PASS: TestBasic4A (15.05s)
=== RUN   TestSpeed4A
Test: ops complete fast enough (4A) ...
  ... Passed --   5.9  3  3127    0
--- PASS: TestSpeed4A (5.92s)
=== RUN   TestConcurrent4A
Test: many clients (4A) ...
  ... Passed --  15.2  5 14620 2792
--- PASS: TestConcurrent4A (15.24s)
=== RUN   TestUnreliable4A
Test: unreliable net, many clients (4A) ...
  ... Passed --  17.7  5  4127  452
--- PASS: TestUnreliable4A (17.70s)
=== RUN   TestUnreliableOneKey4A
Test: concurrent append to same key, unreliable (4A) ...
  ... Passed --   2.4  3   263   52
--- PASS: TestUnreliableOneKey4A (2.39s)
=== RUN   TestOnePartition4A
Test: progress in majority (4A) ...
  ... Passed --   0.8  5    65    2
Test: no progress in minority (4A) ...
  ... Passed --   1.1  5   156    3
Test: completion after heal (4A) ...
  ... Passed --   1.0  5    67    3
--- PASS: TestOnePartition4A (3.50s)
=== RUN   TestManyPartitionsOneClient4A
Test: partitions, one client (4A) ...
  ... Passed --  22.4  5  4920  720
--- PASS: TestManyPartitionsOneClient4A (22.40s)
=== RUN   TestManyPartitionsManyClients4A
Test: partitions, many clients (4A) ...
  ... Passed --  22.5  5  9930 1466
--- PASS: TestManyPartitionsManyClients4A (22.54s)
=== RUN   TestPersistOneClient4A
Test: restarts, one client (4A) ...
  ... Passed --  23.1  5 10499 1912
--- PASS: TestPersistOneClient4A (23.08s)
=== RUN   TestPersistConcurrent4A
Test: restarts, many clients (4A) ...
  ... Passed --  25.1  5 16316 2862
--- PASS: TestPersistConcurrent4A (25.06s)
=== RUN   TestPersistConcurrentUnreliable4A
Test: unreliable net, restarts, many clients (4A) ...
  ... Passed --  22.9  5  4720  468
--- PASS: TestPersistConcurrentUnreliable4A (22.86s)
=== RUN   TestPersistPartition4A
Test: restarts, partitions, many clients (4A) ...
  ... Passed --  29.9  5  9971 1417
--- PASS: TestPersistPartition4A (29.88s)
=== RUN   TestPersistPartitionUnreliable4A
Test: unreliable net, restarts, partitions, many clients (4A) ...
  ... Passed --  28.5  5  4407  297
--- PASS: TestPersistPartitionUnreliable4A (28.47s)
=== RUN   TestPersistPartitionUnreliableLinearizable4A
Test: unreliable net, restarts, partitions, random keys, many clients (4A) ...
  ... Passed --  32.2  7 11361  444
--- PASS: TestPersistPartitionUnreliableLinearizable4A (32.22s)
=== RUN   TestSnapshotRPC4B
Test: InstallSnapshot RPC (4B) ...
labgob warning: Decoding into a non-default variable/field int may not work
  ... Passed --   5.0  3   325   63
--- PASS: TestSnapshotRPC4B (4.96s)
=== RUN   TestSnapshotSize4B
Test: snapshot size is reasonable (4B) ...
  ... Passed --   3.3  3  2467  800
--- PASS: TestSnapshotSize4B (3.30s)
=== RUN   TestSpeed4B
Test: ops complete fast enough (4B) ...
  ... Passed --   4.1  3  3088    0
--- PASS: TestSpeed4B (4.05s)
=== RUN   TestSnapshotRecover4B
Test: restarts, snapshots, one client (4B) ...
  ... Passed --  19.8  5 20631 3982
--- PASS: TestSnapshotRecover4B (19.79s)
=== RUN   TestSnapshotRecoverManyClients4B
Test: restarts, snapshots, many clients (4B) ...
  ... Passed --  20.3  5 96102 18766
--- PASS: TestSnapshotRecoverManyClients4B (20.29s)
=== RUN   TestSnapshotUnreliable4B
Test: unreliable net, snapshots, many clients (4B) ...
  ... Passed --  16.5  5  4103  462
--- PASS: TestSnapshotUnreliable4B (16.48s)
=== RUN   TestSnapshotUnreliableRecover4B
Test: unreliable net, restarts, snapshots, many clients (4B) ...
  ... Passed --  22.3  5  4487  438
--- PASS: TestSnapshotUnreliableRecover4B (22.27s)
=== RUN   TestSnapshotUnreliableRecoverConcurrentPartition4B
Test: unreliable net, restarts, partitions, snapshots, many clients (4B) ...
  ... Passed --  28.9  5  4402  271
--- PASS: TestSnapshotUnreliableRecoverConcurrentPartition4B (28.91s)
=== RUN   TestSnapshotUnreliableRecoverConcurrentPartitionLinearizable4B
Test: unreliable net, restarts, partitions, snapshots, random keys, many clients (4B) ...
  ... Passed --  31.5  7 11331  426
--- PASS: TestSnapshotUnreliableRecoverConcurrentPartitionLinearizable4B (31.53s)
PASS
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>MIT 6.5840（原 6.824) 通关笔记 Lab3 Raft</title><link>https://yomi.moe/posts/mit65840-3/</link><guid isPermaLink="true">https://yomi.moe/posts/mit65840-3/</guid><pubDate>Sat, 23 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Raft是一种分布式一致性算法，目的是在分布式系统中实现可靠的状态机复制。&lt;/p&gt;
&lt;p&gt;从这个Lab开始上强度了，实现和 debug 过程都很繁琐。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;raft2.png&quot; alt=&quot;raft2&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Part 3A: leader election&lt;/h1&gt;
&lt;p&gt;Part A的目标是实现 Leader 选举。&lt;/p&gt;
&lt;h2&gt;三种状态的节点的工作&lt;/h2&gt;
&lt;p&gt;对于 PartA，三种 State 的节点的工作分别为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Follower:  监测自身的心跳状态，如果一段时间内没有收到有效的（term 更大或相等）来自 Leader 的心跳或 Candidate 的投票请求，则变为 Candidate&lt;/li&gt;
&lt;li&gt;Candidate: 成为 Candidate 后立即开始选举，自增 term 并给除自己外的所有节点发送投票请求，同意票超过半数则将自己变为 Leader；如果选举失败或超时，则再次开始选举&lt;/li&gt;
&lt;li&gt;Leader: 不断为其他所有节点发送 AppendEntries RPC&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三类节点相互转换的条件有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Follower: 监测到自身的心跳状态异常时，将自己变为 Candidate&lt;/li&gt;
&lt;li&gt;Candidate: 选举成功后将自己变为 Leader； 若收到 term 大于等于自身的 Leader 的心跳则将自己变为Follower；若收到的心跳或投票请求的term大于自身的term则将自己变为Follower&lt;/li&gt;
&lt;li&gt;Leader:  收到 term 大于自身的投票请求将自己变为 Follower; 若发送的 AppendEntries RPC 得到的 reply 中的 term 大于自身的 term 则将自己变为 Follower&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;具体实现&lt;/h2&gt;
&lt;h3&gt;ticker()与心跳检测&lt;/h3&gt;
&lt;p&gt;Lab框架给出的ticker()作用是每隔随机50~350ms检查当前节点的心跳状态。对于心跳状态的实现，我采用记录每一次收到心跳的时间戳的方式，每次ticker只需要获取当前时间和上一次收到心跳的时间差，然后和超时时间进行对比即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) ticker() {
	for rf.killed() == false {
		// Your code here (3A)
		// Check if a leader election should be started.
		rf.mu.Lock()
		if rf.state == FOLLOWER &amp;amp;&amp;amp; time.Since(rf.electionTimer) &amp;gt; time.Duration(250+(rand.Int63()%100))*time.Millisecond {
			rf.becomeCandidate()
		}
		rf.mu.Unlock()
		// pause for a random amount of time between 50 and 350
		// milliseconds.
		ms := 50 + (rand.Int63() % 300)
		time.Sleep(time.Duration(ms) * time.Millisecond)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一开始实现的时候我是想用Timer做大部分的时间处理，然后就看到了Lab hints中有一条写到：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You&apos;ll need to write code that takes actions periodically or after delays in time. The easiest way to do this is to create a goroutine with a loop that calls &lt;a href=&quot;https://golang.org/pkg/time/#Sleep&quot;&gt;time.Sleep()&lt;/a&gt;; see the &lt;code&gt;ticker()&lt;/code&gt; goroutine that &lt;code&gt;Make()&lt;/code&gt; creates for this purpose. Don&apos;t use Go&apos;s &lt;code&gt;time.Timer&lt;/code&gt; or &lt;code&gt;time.Ticker&lt;/code&gt;, which are difficult to use correctly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以我很干脆的弃用了 Timer，转而使用 Sleep，后面还写出了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go func() {
			time.Sleep(time.Duration(700+(rand.Int63()%300)) * time.Millisecond)
			resultChan &amp;lt;- false
		}()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种看起来有点蠢的代码用来判断选举是否超时，这里我个人觉得肯定是用 After/Timer+select 更优雅。&lt;/p&gt;
&lt;h3&gt;选举&lt;/h3&gt;
&lt;p&gt;按照论文的图2实现即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) becomeCandidate() {
	rf.state = CANDIDATE
	for !rf.killed() {
		rf.currentTerm++
		rf.votedFor = &amp;amp;rf.me
		resultChan := make(chan bool)
		go func() {
			time.Sleep(time.Duration(700+(rand.Int63()%300)) * time.Millisecond)
			resultChan &amp;lt;- false
		}()
		go rf.startElection(len(rf.peers), resultChan)
		if &amp;lt;-resultChan {
			rf.becomeLeader()
			break
		} else {
			rf.mu.Unlock()
			// sleep for a while to update state
			time.Sleep(time.Duration(100+(rand.Int63()%140)) * time.Millisecond)
			rf.mu.Lock()
			if rf.state != CANDIDATE {
				break
			}
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一开始把 rf.becomeLeader() 写进了 startElection() 中，这样做的问题是超时以后原来的协程仍在工作，导致应当被舍弃的选举流程继续下去了，可能会出现多个 Leader。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) startElection(len int, resultChan chan (bool)) {
	voteNum := 1
	finished := 1
	args := RequestVoteArgs{Term: rf.currentTerm, CandidateId: rf.me}
	tempTerm := rf.currentTerm
	termMax := true
	var tempMu sync.Mutex
	cond := sync.NewCond(&amp;amp;tempMu)
	for i := 0; i &amp;lt; len &amp;amp;&amp;amp; termMax; i++ {
		if i == rf.me {
			continue
		}
		go func(i int) {
			var reply RequestVoteReply
			rf.sendRequestVote(i, &amp;amp;args, &amp;amp;reply)
			tempMu.Lock()
			if reply.VoteGranted {
				voteNum++
			}
			if reply.Term &amp;gt; tempTerm {
				tempTerm = reply.Term

			}
			finished++
			tempMu.Unlock()
			cond.Broadcast()
		}(i)
	}
	tempMu.Lock()
	for voteNum &amp;lt;= len/2 &amp;amp;&amp;amp; finished != len &amp;amp;&amp;amp; termMax {
		cond.Wait()
	}
	if voteNum &amp;gt; len/2 &amp;amp;&amp;amp; termMax {
		resultChan &amp;lt;- true
	}
	if !termMax {
		rf.currentTerm = tempTerm
		rf.becomeFollower()
	}
	tempMu.Unlock()
	resultChan &amp;lt;- false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;选举中如果遇到节点的term大于自己，则需要放弃选举并退回到 Follower，更新自己的 term。为了防止竞态，退回到 Follower 和更新 term 需要在协程外进行。&lt;/p&gt;
&lt;h3&gt;投票&lt;/h3&gt;
&lt;p&gt;节点收到投票时，首先比较发起选举的节点term和自己的term：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;发起方&amp;lt;己方：无效选举&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发起方=己方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若己方为 Follower 且无投票对象或投票对象是此次发起方：投票并重置心跳计时器&lt;/li&gt;
&lt;li&gt;若己方为 Candidate：此时己方必定有投票对象自己，拒绝投票&lt;/li&gt;
&lt;li&gt;Leader 永远不会投票&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发起方&amp;gt;己方：投票并更新term，如果己方不是 Follower 就先变成 Follower 再进行投票&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
	// Your code here (3A, 3B).
	rf.mu.Lock()
	defer rf.mu.Unlock()
	reply.Term = rf.currentTerm
	if args.Term &amp;lt; rf.currentTerm {
		reply.VoteGranted = false
		return
	} else if args.Term == rf.currentTerm {
		if (rf.votedFor == nil || *rf.votedFor == args.CandidateId) &amp;amp;&amp;amp; rf.state == FOLLOWER {
			rf.electionTimer = time.Now()
			rf.votedFor = &amp;amp;args.CandidateId
			reply.VoteGranted = true
		} else {
			reply.VoteGranted = false
		}
	} else {
		if rf.state != FOLLOWER {
			rf.becomeFollower()
		}
		rf.currentTerm = args.Term
		rf.votedFor = &amp;amp;args.CandidateId
		reply.VoteGranted = true
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;心跳&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) becomeLeader() {
	rf.state = LEADER
	rf.votedFor = nil
	go rf.leaderWorker()
}

func (rf *Raft) leaderWorker() {
	rf.mu.Lock()
	if rf.state != LEADER {
		rf.mu.Unlock()
		return
	}
	tempTerm := rf.currentTerm
	rf.mu.Unlock()
	var wg sync.WaitGroup
	termMax := true

	for !rf.killed() &amp;amp;&amp;amp; termMax {
		rf.mu.Lock()
		var tempMu sync.Mutex
		args := AppendEntries{Term: rf.currentTerm, LeaderId: rf.me}
		for i := 0; i &amp;lt; len(rf.peers); i++ {
			if i == rf.me {
				continue
			}
			var reply AppendReply
			wg.Add(1)
			go func(i int) {
				rf.sendAppendEntries(i, &amp;amp;args, &amp;amp;reply)
				tempMu.Lock()
				if reply.Term &amp;gt; tempTerm {
					tempTerm = reply.Term
					termMax = false
				}
				tempMu.Unlock()
				wg.Done()
			}(i)
		}
		rf.mu.Unlock()
		ms := 100 + (rand.Int63() % 140)
		time.Sleep(time.Duration(ms) * time.Millisecond)
		wg.Wait()
	}
	if !termMax {
		rf.mu.Lock()
		defer rf.mu.Unlock()
		rf.becomeFollower()
		rf.currentTerm = tempTerm
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和选举类似，如果遇到节点的term大于自己，则需要退回到 Follower，更新自己的 term。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;自己调试过程中得到的几个教训：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注意锁的作用范围，防止死锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;粗粒度锁+循环的部分一定要空一段时间出来让RPC处理获取锁来更新节点的状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多重复测试，有些错误发生概率比较低&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;和我一样使用 print 大法调试的话，多 print 勤 print 不要怕多（   比如我把每一次投票都 print 发现有时候节点的 term 没有被更新，才看到其中一个 args 写成了 reply。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Part 3B: log&lt;/h1&gt;
&lt;p&gt;Part B 要求我们实现Raft系统中日志的可靠复制，也就是论文图2中的所有内容。&lt;/p&gt;
&lt;h2&gt;对PartA的补充&lt;/h2&gt;
&lt;p&gt;添加日志部分后，PartA 中提到的部分机制需要一些修改。&lt;/p&gt;
&lt;p&gt;节点为日志落后的参选者投票。当投票发起方的 term &amp;gt;= 投票者的 term 时， 如果对方的日志不如己方新，则拒绝投票。&lt;/p&gt;
&lt;p&gt;再者，还需要一个协程应用已经 commit 的日志。&lt;/p&gt;
&lt;p&gt;然后就是 AppendEntries，PartA 中我们已经实现了心跳，也就是不包括日志条目的 AppendEntries RPC。PartB的主要工作就是完成这一部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;系统如何保证日志的可靠复制？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当leader节点确认到系统中多数节点收到日志时才会认为日志有效，也就意味着这次被传输的日志已经存在于多数节点&lt;/li&gt;
&lt;li&gt;节点绝不会为日志落后的参选者投票（如果两日志最后条目的有不同term值，那么term较大的日志更新。如果两日志条目term相同，则index较大的日志更新）&lt;/li&gt;
&lt;li&gt;节点收到 term 更大的节点的任何消息后都会更新自己的term&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面三点保证了即使leader节点崩溃以后，新选出的leader一定拥有曾得到commit的日志，然后使这些日志在多数节点得到commit以及执行，并且崩溃一段时间的节点重连以后也不会破坏已经commit的日志。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;leader如何知道其他节点的log中有多少是正确的？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案是慢慢试（&lt;/p&gt;
&lt;p&gt;节点当选leader后就要维护一个叫 nextIndex 的数组，数组每一项记录着下次发送日志在目标节点的附加起点，初始值为leader日志的最大长度。leader 每次发送追加日志请求时都附带待添加条目的前一个条目的 index 和 term，如果目标节点日志的对应 index 处的日志不存在或是 term 不相同，则认为本次添加失败，leader 减少 nextIndex，重复过程直至存在相同 term 的项，将此项以后的所有条目舍弃并添加 leader 发送的待添加日志。&lt;/p&gt;
&lt;p&gt;当然，有时候逐一递减并不是一个好主意，如果目标节点的 commit 日志条目落后太多，leader要发送非常多的追加请求。例子就是 lab 中的 leader backs up quickly over incorrect follower logs 这个 test case，逐一寻找正确的 nextIndex 最终一定会因超时而 fail， 这种情况下我们就需要一些&lt;strong&gt;快速回退机制&lt;/strong&gt;来减少寻找nextIndex的尝试。&lt;/p&gt;
&lt;p&gt;所以按照图2的设计慢慢试的话肯定是不行了（，需要在 AppendEntries RPC 的 Reply 结构中添加更多信息来帮助我们快速回退。&lt;/p&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;其实逐一回退应该也可以通过 Part B，这里我不用快速回退没法通过的原因会在&lt;a href=&quot;./#%E4%BC%98%E5%8C%96&quot;&gt;后面&lt;/a&gt;讲到。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;选举的补充&lt;/h2&gt;
&lt;p&gt;Candidate 的投票 RPC 参数需要添加 LastLogIndex 和 LastLogTerm。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index := len(rf.log) - 1
args := RequestVoteArgs{Term: rf.currentTerm, CandidateId: rf.me, LastLogIndex: index, LastLogTerm: rf.log[index].Term}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而接收者部分按照论文图2实现即可：&lt;/p&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;p&gt;这里的代码有细节错误，应当仅在 follower 投票时才更新计时器，后面实现中已改正。虽然这样也能通过所有 test case，但最好还是按照论文说的做。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
	// Your code here (3A, 3B).
	rf.mu.Lock()
	defer rf.mu.Unlock()
	reply.Term = rf.currentTerm
	if args.Term &amp;lt; rf.currentTerm {
		reply.VoteGranted = false
		return
	}
	if rf.state == FOLLOWER {
		rf.electionTimer = time.Now()
	}
	// requester&apos;s logs are newer
	newer := (args.LastLogTerm == rf.log[len(rf.log)-1].Term &amp;amp;&amp;amp; args.LastLogIndex &amp;gt;= len(rf.log)-1) || args.LastLogTerm &amp;gt; rf.log[len(rf.log)-1].Term
	if args.Term == rf.currentTerm {
		if (rf.votedFor == nil || *rf.votedFor == args.CandidateId) &amp;amp;&amp;amp; newer &amp;amp;&amp;amp; rf.state == FOLLOWER {
			rf.votedFor = &amp;amp;args.CandidateId
			reply.VoteGranted = true
		} else {
			reply.VoteGranted = false
		}
	} else {
		if rf.state != FOLLOWER {
			rf.becomeFollower()
		}
		rf.currentTerm = args.Term
		if newer {
			rf.votedFor = &amp;amp;args.CandidateId
			reply.VoteGranted = true
		} else {
			reply.VoteGranted = false
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Leader的行为&lt;/h2&gt;
&lt;p&gt;Leader 上任后，需要立即发送一次空的追加日志条目 RPC 请求作为初始心跳，然后每隔一段时间重复心跳。如果收到了客户端的请求，则写入日志并同步给各节点。当检测到多数节点成功复制该日志后则提交日志并应用于状态机。&lt;/p&gt;
&lt;p&gt;首先判断各节点上成功复制的最大日志条目 index 是否等于 leader 的日志长度， 如果相等， 即不存在需要复制的条目，则发送心跳，反之则附加待追加条目。  一开始的时候在这里犯蠢了没有给AppendEntries添加超时检测，导致leader心跳那边的 waitGroup 被卡，调试浪费了好长时间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var wg sync.WaitGroup
	termMax := true
	timerCh := make(chan bool)
	for !rf.killed() {
		rf.mu.Lock()
		if rf.state != LEADER {
			rf.mu.Unlock()
			return
		}
		logLen := len(rf.log)
		tempTerm := rf.currentTerm
		var tempMu sync.Mutex
		go func() {
			time.Sleep(time.Duration(100) * time.Millisecond)
			timerCh &amp;lt;- true
		}()
		for i := 0; i &amp;lt; len(rf.peers); i++ {
			if i == rf.me {
				continue
			}
			wg.Add(1)
			go func(i int) {
				var args AppendEntries
				var reply AppendReply
				resultChan := make(chan bool)
				var result bool
				isNeedAppend := rf.matchIndex[i] != logLen-1
				if !isNeedAppend {
					args = AppendEntries{Term: rf.currentTerm, LeaderId: rf.me, PrevLogIndex: rf.nextIndex[i] - 1, PrevLogTerm: rf.log[rf.nextIndex[i]-1].Term, LeaderCommit: rf.commitIndex}
				} else {
					args = AppendEntries{Term: rf.currentTerm, LeaderId: rf.me, PrevLogIndex: rf.nextIndex[i] - 1, PrevLogTerm: rf.log[rf.nextIndex[i]-1].Term, Entries: rf.log[rf.nextIndex[i]:], LeaderCommit: rf.commitIndex}
				}
				go func() {
					time.Sleep(time.Duration(10) * time.Millisecond)
					resultChan &amp;lt;- false
				}()
				go func() {
					result = rf.sendAppendEntries(i, &amp;amp;args, &amp;amp;reply)
					resultChan &amp;lt;- true
				}()
				if &amp;lt;-resultChan {
					tempMu.Lock()
					if reply.Term &amp;gt; tempTerm {
						tempTerm = reply.Term
						termMax = false
					} else if reply.Success {
						rf.nextIndex[i] = logLen
						rf.matchIndex[i] = logLen - 1
					} else if result {
						if reply.RetryIndex &amp;gt;= 0 {
							rf.nextIndex[i] = reply.RetryIndex + 1
						}
						if reply.RetryTerm &amp;gt;= 0 {
							rf.leaderFindNextIndex(i, reply.RetryTerm)
						}
					}
					tempMu.Unlock()
				}
				wg.Done()
			}(i)
		}
		wg.Wait()
		if termMax {
			rf.updateCommitIndex()
		} else {
			rf.becomeFollower()
			rf.currentTerm = tempTerm
			rf.mu.Unlock()
			&amp;lt;-timerCh
			return
		}
		rf.mu.Unlock()
		&amp;lt;-timerCh
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我采用的回退机制是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不存在 prevLogIndex 对应的条目：告知 leader 节点目前的最后一项日志的 index，leader 下次从该 index 对应的条目开始尝试。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./#%E6%9B%B4%E5%BF%AB%E7%9A%84%E5%9B%9E%E9%80%80&quot;&gt;如果 prevLogIndex 存在的条目的 term 不同&lt;/a&gt;：告知 leader 当前 index 处条目的实际 term，leader 下次从该 term 或更低的 term 开始尝试。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;	reply.RetryTerm = -1
	reply.RetryIndex = -1
	if len(rf.log) &amp;lt;= args.PrevLogIndex {
		reply.Success = false
		reply.RetryIndex = len(rf.log) - 1
		return
	}

	if rf.log[args.PrevLogIndex].Term != args.PrevLogTerm {
		reply.Success = false
		reply.RetryTerm = rf.log[args.PrevLogIndex].Term
		return
	}

	reply.Success = true
	rf.currentTerm = args.Term
	if rf.state != FOLLOWER {
		rf.becomeFollower()
	}

	if len(args.Entries) &amp;gt; 0 {
		rf.log = append(rf.log[:args.PrevLogIndex+1], args.Entries...)
	}
	if args.LeaderCommit &amp;gt; rf.commitIndex {
		rf.commitIndex = min(len(rf.log)-1, args.LeaderCommit)
		rf.commitCond.Broadcast()
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次 commitIndex 有更新时都会提示 applier 协程应用已提交日志，不过后来我还是觉得不用 Cond 而是用循环不断检测 commitIndex 更好些（&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;做完感觉难度其实也没有网传的那么夸张，不过调试确实挺痛苦的。&lt;/p&gt;
&lt;p&gt;有些问题发生率只有百分之x甚至千分之x，&lt;a href=&quot;https://gist.github.com/jonhoo/f686cacb4b9fe716d5aa&quot;&gt;这个脚本&lt;/a&gt;对发现问题会很有帮助（&lt;/p&gt;
&lt;h1&gt;Part 3C: persistence&lt;/h1&gt;
&lt;p&gt;&lt;s&gt;改了 Make() 以后忘了初始化 follower 的选举计时器，导致连 PartB 都有概率过不了又回去改了俩小时才发现问题，各位不要学我（&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;更快的回退&lt;/h2&gt;
&lt;p&gt;实现了 persist() 和 readPersist() 以后首次测试只有 Figure 8 (unreliable) 这个 case Fail 了，后面又测试了 500 次，也确实只有这一个 case 有问题。然而 case 和 persistence 没有关系， 完全没有涉及到状态的保存和恢复，所以这个 fail 的原因完全是因为我的 partB 不够完善（&lt;/p&gt;
&lt;p&gt;看了一下 tester 的代码，基本流程是首先添加一条指令并得到确认，然后开始一千次迭代，每次迭代首先向所有自认为是 leader 的节点发送一条指令，然后进行休眠，小概率休眠数百毫秒，大概率休眠不到十毫秒，结束后有概率断开最大序号的 leader 节点， 若断开后节点数量不足 3，则随机选择一个节点，若该节点处于断开状态则重连。当迭代次数达到 200 时，启用长时间的消息重排以模拟网络不稳定的情况。迭代结束后重连所有断开的节点，提交一个新指令，要求 10s 内完成该指令的同步。&lt;/p&gt;
&lt;p&gt;造成失败的原因是&lt;a href=&quot;./#leader%E7%9A%84%E8%A1%8C%E4%B8%BA&quot;&gt;之前提到的回退机制&lt;/a&gt;在目标节点对应 index 的日志条目 term 高于 leader 的日志条目时仍是逐一回退。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不存在 prevLogIndex 对应的条目：告知 leader 节点目前的最后一项日志的 index，leader 下次从该 index 对应的条目开始尝试。&lt;/li&gt;
&lt;li&gt;如果 prevLogIndex 存在的条目的 term 不同：
&lt;ul&gt;
&lt;li&gt;leader 日志条目 term 更大：告知 leader 当前 index 处条目的实际 term，leader 下次从该 term 或更低的 term 的条目开始尝试。&lt;/li&gt;
&lt;li&gt;当前节点日志条目的 term 更大： 搜索本地日志中 term &amp;lt;= prevLogTerm 的最大 index 条目，leader 下次从该 index 对应的条目开始尝试。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) RespondAppendEntries(args *AppendEntries, reply *AppendReply) {
	rf.mu.Lock()
	defer rf.mu.Unlock()

	reply.Term = rf.CurrentTerm
	if args.Term &amp;lt; rf.CurrentTerm {
		return
	}
	if rf.state == FOLLOWER {
		rf.electionTimer = time.Now()
	} else {
		rf.becomeFollower()
	}
	rf.CurrentTerm = args.Term
	rf.persist()
	reply.RetryTerm = -1
	reply.RetryIndex = -1

	if len(rf.Log) &amp;lt;= args.PrevLogIndex {
		reply.Success = false
		reply.RetryIndex = len(rf.Log) - 1
		return
	}
	if rf.Log[args.PrevLogIndex].Term != args.PrevLogTerm {
		reply.Success = false
		if rf.Log[args.PrevLogIndex].Term &amp;gt; args.PrevLogTerm {
			for i := args.PrevLogIndex-1; i &amp;gt;= 0; i-- {
				if rf.Log[i].Term &amp;lt;= args.PrevLogTerm {
					reply.RetryIndex = i
					return
				}
			}
		} else {
			reply.RetryTerm = rf.Log[args.PrevLogIndex].Term
		}
		return
	}
	reply.Success = true
	if len(args.Entries) &amp;gt; 0 {
		rf.Log = append(rf.Log[:args.PrevLogIndex+1], args.Entries...)
	}
	rf.persist()
	if args.LeaderCommit &amp;gt; rf.commitIndex {
		rf.commitIndex = min(len(rf.Log)-1, args.LeaderCommit)
		rf.commitCond.Broadcast()
	}

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更改后顺利 pass。&lt;/p&gt;
&lt;h2&gt;优化&lt;/h2&gt;
&lt;p&gt;虽说是全部通过了，但是速度慢到感人。单单一个 Figure 8 (unreliable) 就要花两分钟上下，不优化不行了。&lt;/p&gt;
&lt;p&gt;首先对 Figure 8 (unreliable) 的时间占用进行调试， 发现在千次迭代中起初遍历所有节点并向 leader 添加指令的操作占用时间很长，最高能达到 600ms 以上，但最低值仅有 5ms。&lt;/p&gt;
&lt;p&gt;那问题就好说了，出现这种情况原因只能是 Start() 占用了太长时间，而其本身也没有任何复杂操作，肯定是其他操作占用锁阻塞时长，数百毫秒的时长占用也只能是 Candidate 导致的。&lt;/p&gt;
&lt;p&gt;更改竞选开始时锁的解锁位置，使竞选过程中也能回复 Start()，并且在选举结束后再次检查当前节点状态，避免出现多个 leader。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) becomeCandidate() {
	rf.state = CANDIDATE
	for !rf.killed() {
		rf.CurrentTerm++
		rf.VotedFor = rf.me
		rf.persist()
		rf.mu.Unlock()
		resultChan := make(chan bool)
		go func() {
			time.Sleep(time.Duration(500+(rand.Int63()%100)) * time.Millisecond)
			resultChan &amp;lt;- false
		}()
		go rf.startElection(len(rf.peers), resultChan)
		if &amp;lt;-resultChan {
			rf.mu.Lock()
			if rf.state == CANDIDATE {
				rf.becomeLeader()
			}
			break
		} else {
			// sleep for a while to update state
			time.Sleep(time.Duration(120+(rand.Int63()%80)) * time.Millisecond)
			rf.mu.Lock()
			if rf.state != CANDIDATE {
				break
			}
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后完成一次 3C test 总时长约 2min30s。&lt;/p&gt;
&lt;h2&gt;具体实现&lt;/h2&gt;
&lt;p&gt;这个 part 应该叫做 Figure 8 而不是 persistence。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) persist() {
	// Your code here (3C).
	// Example:
	// w := new(bytes.Buffer)
	// e := labgob.NewEncoder(w)
	// e.Encode(rf.xxx)
	// e.Encode(rf.yyy)
	// raftstate := w.Bytes()
	// rf.persister.Save(raftstate, nil)

	//log currentTerm voteFor
	w := new(bytes.Buffer)
	e := labgob.NewEncoder(w)
	e.Encode(rf.Log)
	e.Encode(rf.CurrentTerm)
	e.Encode(rf.VotedFor)

	raftstate := w.Bytes()
	rf.persister.Save(raftstate, nil)
}

// restore previously persisted state.
func (rf *Raft) readPersist(data []byte) {
	if data == nil || len(data) &amp;lt; 1 { // bootstrap without any state?
		return
	}
	// Your code here (3C).
	// Example:
	// r := bytes.NewBuffer(data)
	// d := labgob.NewDecoder(r)
	// var xxx
	// var yyy
	// if d.Decode(&amp;amp;xxx) != nil ||
	//    d.Decode(&amp;amp;yyy) != nil {
	//   error...
	// } else {
	//   rf.xxx = xxx
	//   rf.yyy = yyy
	// }
	r := bytes.NewBuffer(data)
	d := labgob.NewDecoder(r)
	var Log []Log
	var CurrentTerm int
	var VotedFor int
	if d.Decode(&amp;amp;Log) != nil || d.Decode(&amp;amp;CurrentTerm) != nil || d.Decode(&amp;amp;VotedFor) != nil {
		DPrintf(&quot;error!\n&quot;)
	} else {
		rf.CurrentTerm = CurrentTerm
		rf.Log = Log
		rf.VotedFor = VotedFor
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Part 3D: log compaction&lt;/h1&gt;
&lt;p&gt;Part D是思路最清晰同时做起来最难受的一部分。&lt;/p&gt;
&lt;p&gt;要做的事情可以分为三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实现快照的接收与保存。&lt;/li&gt;
&lt;li&gt;实现 leader 对 follower 节点快照状态的检测以及相关 RPC 处理。&lt;/li&gt;
&lt;li&gt;处理日志的索引等相关参数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最痛苦的部分就在于第三步中巨量的越界错误。&lt;/p&gt;
&lt;h2&gt;具体实现&lt;/h2&gt;
&lt;h3&gt;快照&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) Snapshot(index int, snapshot []byte) {
	// Your code here (3D).
	rf.mu.Lock()
	defer rf.mu.Unlock()
	if index &amp;gt; 0 {
		rf.SnapshotState.SnapshotTerm = rf.Log[index-rf.SnapshotState.SnapshotIndex].Term
		rf.Log = rf.Log[index-rf.SnapshotState.SnapshotIndex:]
		rf.Log[0] = Log{Term: rf.SnapshotState.SnapshotTerm}
		if rf.state == LEADER {
			for i := range rf.nextIndex {
				if i == rf.me {
					continue
				}
				rf.nextIndex[i] = len(rf.Log) + index
			}
		}
		rf.SnapshotState.SnapshotIndex = index
	}
	rf.SnapshotState.SnapshotSave = snapshot
	rf.persist()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接收快照会改变日志的长度， 所以 nextIndex 的长度也要进行相应修改。&lt;/p&gt;
&lt;p&gt;类似的，在初始化 nextIndex 时除了 log 的长度，还要加上快照涵盖的日志长度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) becomeLeader() {
	rf.state = LEADER
	rf.VotedFor = NULL
	rf.persist()
	rf.nextIndex = make([]int, len(rf.peers))
	for i := range rf.nextIndex {
		rf.nextIndex[i] = len(rf.Log) + rf.SnapshotState.SnapshotIndex
	}
	rf.matchIndex = make([]int, len(rf.peers))
	go func() {
		rf.leaderFirstHeartBeat()
		rf.leaderWorker()
	}()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;索引&lt;/h3&gt;
&lt;p&gt;在 A ~ C 中我们使用了很多日志索引来定位日志，而引入快照之后日志的实际索引会有偏移，偏移值为快照涵盖的日志长度。所以我们需要记录这个长度并对其进行持久化。&lt;/p&gt;
&lt;p&gt;例如在判断是否投票时需要对比日志的新旧：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	newer := (args.LastLogTerm == rf.Log[len(rf.Log)-1].Term &amp;amp;&amp;amp; args.LastLogIndex &amp;gt;= len(rf.Log)-1+rf.SnapshotState.SnapshotIndex) || args.LastLogTerm &amp;gt; rf.Log[len(rf.Log)-1].Term
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以及上述提到的 nextIndex的值都需要注意，这部分是整个 lab 最容易出错的地方。&lt;/p&gt;
&lt;h3&gt;快照的发送&lt;/h3&gt;
&lt;p&gt;当回退无法找到对应的日志条目时， leader 需要向节点发送快照。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
else if result {
	if reply.RetryIndex &amp;gt;= 0 {
		if reply.RetryIndex &amp;lt; rf.SnapshotState.SnapshotIndex {
			replyTerm, result := rf.SendInstallSnapshot(i)
			if result {
				if replyTerm &amp;gt; rf.CurrentTerm {
					termMax = false
				} else {
					rf.nextIndex[i] = logLen + rf.SnapshotState.SnapshotIndex
				}
				tempMu.Unlock()
			}
			wg.Done()
			return
		} else {
			rf.nextIndex[i] = reply.RetryIndex + 1
		}
	}
	if reply.RetryTerm &amp;gt;= 0 {
		if !rf.leaderFindNextIndex(i, reply.RetryTerm) {
            ...
            // 同上
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Follower 的处理按照论文中的图 13 实现即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) RespondInstallSnapshot(args *SnapshotArgs, reply *SnapshotReply) {
	rf.mu.Lock()
	defer rf.mu.Unlock()
	reply.Term = rf.CurrentTerm
	if args.Term &amp;lt; rf.CurrentTerm {
		return
	} else if rf.state != FOLLOWER {
		rf.becomeFollower()
	} else {
		rf.electionTimer = time.Now()
	}
	if args.LastIncludedIndex &amp;lt; len(rf.Log) &amp;amp;&amp;amp; rf.Log[args.LastIncludedIndex].Term == args.LastIncludedTerm {
		return
	}
	rf.Log = make([]Log, 1, 128)
	rf.Log[0] = Log{Term: args.LastIncludedTerm}
	rf.SnapshotState = SnapshotState{SnapshotSave: args.Data, SnapshotIndex: args.LastIncludedIndex, SnapshotTerm: args.LastIncludedTerm}
	rf.applyCh &amp;lt;- ApplyMsg{CommandValid: false, SnapshotValid: true, Snapshot: rf.SnapshotState.SnapshotSave, SnapshotTerm: rf.SnapshotState.SnapshotTerm, SnapshotIndex: rf.SnapshotState.SnapshotIndex}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日志的初始项 term 不能设置成 0 了，需要设置成快照 term 来对齐条目的查找。&lt;/p&gt;
&lt;h2&gt;持久化&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) persist() {
	w := new(bytes.Buffer)
	e := labgob.NewEncoder(w)
	e.Encode(rf.Log)
	e.Encode(rf.CurrentTerm)
	e.Encode(rf.VotedFor)
	e.Encode(rf.SnapshotState.SnapshotTerm)
	e.Encode(rf.SnapshotState.SnapshotIndex)
	raftstate := w.Bytes()

	if rf.SnapshotState.SnapshotIndex == 0 {
		rf.persister.Save(raftstate, nil)
	} else {
		rf.persister.Save(raftstate, rf.SnapshotState.SnapshotSave)
	}
}

func (rf *Raft) readPersist(data []byte) {
	if data == nil || len(data) &amp;lt; 1 { // bootstrap without any state?
		return
	}
	r := bytes.NewBuffer(data)
	d := labgob.NewDecoder(r)
	var Log []Log
	var CurrentTerm int
	var VotedFor int
	var snapshotTerm int
	var snapshotIndex int
	if d.Decode(&amp;amp;Log) != nil || d.Decode(&amp;amp;CurrentTerm) != nil || d.Decode(&amp;amp;VotedFor) != nil || d.Decode(&amp;amp;snapshotTerm) != nil || d.Decode(&amp;amp;snapshotIndex) != nil {
		DPrintf(&quot;error!\n&quot;)
	} else {
		rf.CurrentTerm = CurrentTerm
		rf.Log = Log
		rf.VotedFor = VotedFor
		rf.SnapshotState.SnapshotTerm = snapshotTerm
		rf.SnapshotState.SnapshotIndex = snapshotIndex
		rf.commitIndex = snapshotIndex
	}
}
func (rf *Raft) readSnapShot(data []byte) {
	if data == nil || len(data) &amp;lt; 1 { // bootstrap without any state?
		return
	}
	rf.SnapshotState.SnapshotSave = data
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;persister.save() 的第二项参数直接填入快照，节点初始化时调用 rf.readSnapShot(persister.ReadSnapshot()) 即可。&lt;/p&gt;
&lt;h2&gt;死锁&lt;/h2&gt;
&lt;p&gt;测试时总是有节点莫名其妙阻塞，Debug 发现测试程序会停止 applyCh 的接收而要求节点接收快照，从而导致死锁。&lt;/p&gt;
&lt;p&gt;可以采用 select 方法，在 applyCh 没有接收的时候暂时解锁，记得把 lastApplied 减回去（&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (rf *Raft) apply() {
	for !rf.killed() {
		rf.mu.Lock()
		rf.commitCond.Wait()
		if rf.lastApplied &amp;lt; rf.SnapshotState.SnapshotIndex {
			rf.lastApplied = rf.SnapshotState.SnapshotIndex
		}
		for rf.commitIndex &amp;gt; rf.lastApplied {
			rf.lastApplied++
			select {
			case rf.applyCh &amp;lt;- ApplyMsg{CommandValid: true, Command: rf.Log[rf.lastApplied-rf.SnapshotState.SnapshotIndex].Command, CommandIndex: rf.lastApplied}:
			default:
				rf.lastApplied--
				rf.mu.Unlock()
				time.Sleep(100 * time.Microsecond)
				rf.mu.Lock()
			}
		}
		rf.mu.Unlock()
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;一路做下来感觉最大的难点还是在于寻找 bug，比如&lt;a href=&quot;./#%E6%AD%BB%E9%94%81&quot;&gt;这一部分&lt;/a&gt;写出来短短两行，但实际上找出这个 bug 花了我半天时间。所以不仅要熟悉自己的代码，还要理清大部分测试代码的逻辑。&lt;/p&gt;
&lt;p&gt;其次要注意锁的粒度，锁中套锁这种低级错误比较容易发现，更需要注意的，同时也是 lab 的 hint 中提到的：不要进行完全不间断的循环。&lt;/p&gt;
&lt;p&gt;所有测试最好都用&lt;a href=&quot;./#%E6%80%BB%E7%BB%93-1&quot;&gt;助教提供的脚本&lt;/a&gt;进行 2k+ 次，过程中使用 util.go 中提供的 DPrint 记录下日志，有些 bug 说成可遇不可求也毫不夸张。&lt;/p&gt;
&lt;p&gt;附单次测试结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ time go test -run 3
Test (3A): initial election ...
  ... Passed --   3.5  3   62   15852    0
Test (3A): election after network failure ...
  ... Passed --   5.1  3  129   24168    0
Test (3A): multiple elections ...
  ... Passed --   7.0  7  740  135792    0
Test (3B): basic agreement ...
  ... Passed --   1.1  3   16    4124    3
Test (3B): RPC byte count ...
  ... Passed --   2.6  3   48  113064   11
Test (3B): test progressive failure of followers ...
  ... Passed --   5.1  3  110   22962    3
Test (3B): test failure of leaders ...
  ... Passed --   5.6  3  192   38864    3
Test (3B): agreement after follower reconnects ...
  ... Passed --   6.4  3  122   29674    7
Test (3B): no agreement if too many followers disconnect ...
  ... Passed --   4.2  5  186   37984    3
Test (3B): concurrent Start()s ...
  ... Passed --   0.6  3    8    2052    6
Test (3B): rejoin of partitioned leader ...
  ... Passed --   4.8  3  144   31017    4
Test (3B): leader backs up quickly over incorrect follower logs ...
  ... Passed --  27.2  5 2123 1621069  102
Test (3B): RPC counts aren&apos;t too high ...
  ... Passed --   2.2  3   38   10226   12
Test (3C): basic persistence ...
  ... Passed --   4.6  3   80   19066    6
Test (3C): more persistence ...
  ... Passed --  18.6  5  996  204870   16
Test (3C): partitioned leader and one follower crash, leader restarts ...
  ... Passed --   2.5  3   38    9018    4
Test (3C): Figure 8 ...
  ... Passed --  29.2  5  672  127664   18
Test (3C): unreliable agreement ...
  ... Passed --   5.6  5  212   69514  246
Test (3C): Figure 8 (unreliable) ...
  ... Passed --  40.9  5 3548 9105166  318
Test (3C): churn ...
  ... Passed --  16.2  5  904  491678  222
Test (3C): unreliable churn ...
  ... Passed --  16.2  5  604  233967  105
Test (3D): snapshots basic ...
  ... Passed --   7.3  3  138   45466  210
Test (3D): install snapshots (disconnect) ...
  ... Passed --  62.2  3 1408  462099  296
Test (3D): install snapshots (disconnect+unreliable) ...
  ... Passed --  71.0  3 1603  494791  352
Test (3D): install snapshots (crash) ...
  ... Passed --  41.4  3  784  286346  352
Test (3D): install snapshots (unreliable+crash) ...
  ... Passed --  48.1  3  874  321159  354
Test (3D): crash and restart all servers ...
  ... Passed --  15.6  3  276   74866   63
Test (3D): snapshot initialization after crash ...
  ... Passed --   4.4  3   68   17990   14
PASS
ok  	6.5840/raft	459.328s

real	7m39.596s
user	0m9.691s
sys	0m1.889s
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>MIT 6.5840（原 6.824) 通关笔记 Lab2 Key/Value Server</title><link>https://yomi.moe/posts/mit65840-2/</link><guid isPermaLink="true">https://yomi.moe/posts/mit65840-2/</guid><description>这门课2024新增的充数Lab</description><pubDate>Sat, 16 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这个Lab是Spring 2024的新Lab，难度十分低。&lt;/p&gt;
&lt;h1&gt;Key/value server with no network failures&lt;/h1&gt;
&lt;p&gt;太简单了以至于没什么好记录的，no network failures意味着实现线性化只需要加锁就可以了。&lt;/p&gt;
&lt;h1&gt;Key/value server with dropped messages&lt;/h1&gt;
&lt;p&gt;实验要求是server再次收到已完成过的请求时丢弃重复请求不处理，只需要用map记录下处理过且未经客户端确认的请求，在收到客户端ACK后清理即可。&lt;/p&gt;
&lt;h2&gt;内存释放&lt;/h2&gt;
&lt;p&gt;起初是使用了map存储nrand()生成的操作id和对应的append操作发生前的结果字符串，但是这样没法通过memory use many appends这个case。然后注意到其实append操作不会修改之前的存在结果，所以历史map可以改为存储append前的value字符串长度，这样就可以将string改为int以此节省内存。&lt;/p&gt;
&lt;h2&gt;server代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type KVServer struct {
	mu sync.Mutex
	// Your definitions here.
	pairs   map[string]string
	history map[int64]int
}

func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
	// Your code here.
	kv.mu.Lock()
	defer kv.mu.Unlock()

	reply.Value = kv.pairs[args.Key]

}

func (kv *KVServer) Put(args *PutAppendArgs, reply *PutAppendReply) {
	// Your code here.

	kv.mu.Lock()
	defer kv.mu.Unlock()
	kv.pairs[args.Key] = args.Value
	delete(kv.history, args.ID)
}

func (kv *KVServer) Append(args *PutAppendArgs, reply *PutAppendReply) {
	// Your code here.
	kv.mu.Lock()
	defer kv.mu.Unlock()
	if args.Report {
		delete(kv.history, args.ID)
		return
	}
	if val, ok := kv.history[args.ID]; ok {
		reply.Value = kv.pairs[args.Key][:val]
		return
	}
	reply.Value = kv.pairs[args.Key]
	kv.history[args.ID] = len(reply.Value)
	kv.pairs[args.Key] += args.Value
}

func StartKVServer() *KVServer {
	kv := new(KVServer)

	// You may need initialization code here.
	kv.pairs = make(map[string]string)
	kv.history = make(map[int64]int)

	return kv
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>MIT 6.5840（原 6.824) 通关笔记 Lab1 MapReduce</title><link>https://yomi.moe/posts/mit65840-1/</link><guid isPermaLink="true">https://yomi.moe/posts/mit65840-1/</guid><description>简单记录一下学习6.5840过程中的一些坑和解决思路</description><pubDate>Fri, 15 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;挺早就听闻MIT6.824的大名，趁着这段时间比较闲，一边学习一边记录一下实验的完成过程。&lt;/p&gt;
&lt;h1&gt;MapReduce&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;./MapReduce.jpg&quot; alt=&quot;MapReduce&quot; /&gt;&lt;/p&gt;
&lt;p&gt;MapReduce是一个用于大规模数据集并行处理的分布式计算框架。&lt;/p&gt;
&lt;p&gt;步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从分布式文件系统中加载文件并进行分片 ( Split ) .&lt;/li&gt;
&lt;li&gt;Master 节点通过心跳机制检测Worker节点状态，并分配任务给 Worker 节点。&lt;/li&gt;
&lt;li&gt;Worker 节点执行 Map 任务，将原始分配文件转换为中间键值对。随后，这些键值对会经过分区并在 shuffle 阶段进行分组和排序。&lt;/li&gt;
&lt;li&gt;Map 阶段结束后，Reduce Worker 接收分组后的键值对数据，执行 Reduce 操作并将最终结果文件输出到分布式文件系统中。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Lab1&lt;/h1&gt;
&lt;p&gt;Lab1的要求是在提供的code框架基础上，实现worker节点和Coordinator ( Master ) 节点的基本功能。&lt;/p&gt;
&lt;p&gt;总体难度不高，思路也比较清晰。如果之前没怎么接触过go建议先看一下课程的LEC 5了解golang的并发设计方式。&lt;/p&gt;
&lt;h2&gt;Worker&lt;/h2&gt;
&lt;p&gt;对于 worker 节点，按照 hints 里的建议，首先修改Worker()，向Master节点发送RPC请求任务，得到任务后根据任务类型进行不同的操作。实验要求当没有任务时Worker应该退出， 这里我直接采用了 reply 中没有任务时Worker 自行结束的方案。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main/mrworker.go calls this function.
func Worker(mapf func(string, string) []KeyValue,
	reducef func(string, []string) string) {
	// Your worker implementation here.
	// uncomment to send the Example RPC to the coordinator.
	// CallExample()
	for {
		args := Args{}
		reply := Reply{}
		call(&quot;Coordinator.RPCMaster&quot;, &amp;amp;args, &amp;amp;reply)
		if reply.MapTask != nil {
			doMapTask(mapf, reply.MapTask, reply.NReduce)
		} else if reply.ReduceTask != nil {
			doReduceTask(reducef, reply.ReduceTask, reply.FilesLen)
		} else {
			return
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Map阶段&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func doMapTask(mapf func(string, string) []KeyValue, task *MapTask, nReduce int) error {
	file, err := os.Open(task.Filename)
	if err != nil {
		log.Fatalf(&quot;cannot open %v&quot;, task.Filename)
	}
	content, err := ioutil.ReadAll(file)
	if err != nil {
		log.Fatalf(&quot;cannont read %v&quot;, task.Filename)
	}
	file.Close()

	kva := mapf(task.Filename, string(content))
	sort.Sort(ByKey(kva))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文件操作我们可以直接 copy mrsequential.go 中提供的部分。重点是分区操作，需要根据键的哈希值将键值对分配到对应的 Reduce 分区。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;omap := make([][]KeyValue, nReduce)
	for _, kv := range kva {
		reduceNum := ihash(kv.Key) % nReduce
		omap[reduceNum] = append(omap[reduceNum], kv)
	}
	for i := 0; i &amp;lt; nReduce; i++ {
		intermediateFileName := fmt.Sprintf(&quot;map-%d-%d.json&quot;, task.Num, i)
		tempFile, err := ioutil.TempFile(&quot;.&quot;, &quot;tmp-&quot;)
		tempFileName := tempFile.Name()
		if err != nil {
			return fmt.Errorf(&quot;failed to create temp file: %w&quot;, err)
		}
		enc := json.NewEncoder(tempFile)
		for _, kv := range omap[i] {
			err := enc.Encode(&amp;amp;kv)
			if err != nil {
				return fmt.Errorf(&quot;failed to create json file: %w&quot;, err)
			}
		}
		tempFile.Close()
		if err := os.Rename(tempFileName, intermediateFileName); err != nil {
			return fmt.Errorf(&quot;failed to rename temp file to target file: %w&quot;, err)
		}
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;omap 数组存储本次 Map 结果不同键应处的桶，创建对应文件并写入。最后，我们需要让 Master 知道这个节点的任务已经完成，所以我们还需要定义另一个 RPC 函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;		args := task
		var reply Reply
		call(&quot;Coordinator.MapTaskComplete&quot;, &amp;amp;args, &amp;amp;reply)
	
		return nil
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reduce阶段&lt;/h3&gt;
&lt;p&gt;Reduce 同理，其实实现 Worker 部分的注意点在实验 hints 的讲述都比较详细了， 具体的实现也写在了  mrsequential.go 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func doReduceTask(reducef func(string, []string) string, task *ReduceTask, fileLens int) error {
	var kva []KeyValue
	for i := 0; i &amp;lt; fileLens; i++ {
		intermediateFileName := fmt.Sprintf(&quot;map-%d-%d.json&quot;, i, task.Num)
		file, err := os.Open(intermediateFileName)
		if err != nil &amp;amp;&amp;amp; !os.IsNotExist(err) {
			return fmt.Errorf(&quot;Error opening file: %v\n&quot;, err)
		}
		dec := json.NewDecoder(file)
		for {
			var kv KeyValue
			if err := dec.Decode(&amp;amp;kv); err != nil {
				break
			}
			kva = append(kva, kv)
		}
	}
	sort.Sort(ByKey(kva))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;收集任务 id 对应的 Map 任务的中间结果文件，然后 copy 一下 mrsequential.go 中具体的归约操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tempFile, err := ioutil.TempFile(&quot;.&quot;, &quot;tmp-&quot;)
	tempFileName := tempFile.Name()
	if err != nil {
		return fmt.Errorf(&quot;failed to create temp file: %w&quot;, err)
	}
	oname := fmt.Sprintf(&quot;mr-out-%d&quot;, task.Num)
	i := 0
	for i &amp;lt; len(kva) {
		j := i + 1
		for j &amp;lt; len(kva) &amp;amp;&amp;amp; kva[j].Key == kva[i].Key {
			j++
		}
		values := []string{}
		for k := i; k &amp;lt; j; k++ {
			values = append(values, kva[k].Value)
		}
		output := reducef(kva[i].Key, values)

		fmt.Fprintf(tempFile, &quot;%v %v\n&quot;, kva[i].Key, output)
		i = j
	}
	tempFile.Close()
	if err := os.Rename(tempFileName, oname); err != nil {
		return fmt.Errorf(&quot;failed to rename temp file to target file: %w&quot;, err)
	}
	args := task
	var reply Reply
	call(&quot;Coordinator.ReduceTaskComplete&quot;, &amp;amp;args, &amp;amp;reply)
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，和 Map 阶段一致， 我们也需要 RPC 通知 Master 节点任务完成。&lt;/p&gt;
&lt;h2&gt;Coordinator&lt;/h2&gt;
&lt;p&gt;我使用的 Master 结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Coordinator struct {
	// Your definitions here.
	mapCompletedTaskNums    int
	reduceCompletedTaskNums int
	nReduce                 int
	mapTaskCompleted        bool
	reduceTaskCompleted     bool
	mapTasks                []*MapTask
	reduceTasks             []*ReduceTask
	mu                      sync.Mutex
	cond                    *sync.Cond
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;思路就是 Master 维护两个任务列表， 在 Worker 请求任务时将任务分配过去并监测任务的状态。 任务失败则改回任务状态等待下一个来请求任务的节点， 任务成功则更改任务状态为成功并检查所有的阶段任务 ( Map/Reduce ) 是否全部完成。&lt;/p&gt;
&lt;p&gt;（代码写得不怎么优雅，见谅）&lt;/p&gt;
&lt;p&gt;RPC应答函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (c *Coordinator) RPCMaster(args *Args, reply *Reply) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	for { //map
		if c.mapTaskCompleted {
			break
		} else if task := c.fetchMapTask(); task != nil {
			reply.MapTask = task
			reply.NReduce = c.nReduce
			c.mapTaskStart(task)
			return nil
		} else {
			c.cond.Wait()
		}
	}
	for { //reduce
		if c.reduceTaskCompleted {
			break
		} else if task := c.fetchReduceTask(); task != nil {
			reply.ReduceTask = task
			reply.FilesLen = len(c.mapTasks)
			c.reduceTaskStart(task)
			return nil
		} else {
			c.cond.Wait()
		}
	}
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;任务开始函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (c *Coordinator) mapTaskStart(task *MapTask) {
	task.State = STARTED
	go func(task *MapTask) {
		timedue := time.After(10 * time.Second)
		&amp;lt;-timedue
		c.mu.Lock()
		defer c.mu.Unlock()
		if task.State != FINISHED {
			log.Printf(&quot;recover map task %d \n&quot;, task.Num)
			task.State = WAITTING
			c.cond.Broadcast()
		}
	}(task)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计时器检查任务状态，若失败则改回任务状态并通知RPC协程停止阻塞。&lt;/p&gt;
&lt;p&gt;任务完成函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (c *Coordinator) MapTaskComplete(task *MapTask, reply *Reply) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.mapTasks[task.Num].State = FINISHED
	c.mapCompletedTaskNums++
	if c.mapCompletedTaskNums == len(c.mapTasks) {
		c.mapTaskCompleted = true
		c.cond.Broadcast()
	}
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Worker完成任务后RPC调用此函数更改任务状态。&lt;/p&gt;
</content:encoded></item></channel></rss>