<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>文章 - 硬盘在歌唱 on 硬盘在歌唱</title><link>http://disksing.com/post/</link><description>Recent content in 文章 - 硬盘在歌唱 on 硬盘在歌唱</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Mon, 16 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="http://disksing.com/post/index.xml" rel="self" type="application/rss+xml"/><item><title>Open Prompt：AI 时代的 PR 协作新范式</title><link>http://disksing.com/open-prompt/</link><pubDate>Mon, 16 Mar 2026 00:00:00 +0000</pubDate><guid>http://disksing.com/open-prompt/</guid><description>最近大家都在感慨：“Code is cheap，Markdown 才是新时代的源代码。”
我就想，传统的基于源代码的 Open Source 协作方式显然已经不太奏效了。因为源代码相当于是新时代的 Binary，围绕程序生成出来的东西该怎么协作？
我感受到最明显的痛点是代码 Review 的瘫痪。
写代码的时候，Claude/Codex “一把梭”又快又稳；Review 的时候，一个 PR 动辄大几千行，根本没法看。Reviewer 往往只能硬着头皮让 AI 检查一遍，再把结果复制粘贴回去交差。
既然传统的代码协作已经走不通，要解决这个问题，多人协作的基础就必须回到真正的源头——也就是 Prompt。
我们不妨把这个流派叫作 Open Prompt。
而且这套工作流实现起来并不复杂，也不需要发明什么全新的平台。GitHub、PR、Comment、CI、Agent 这些基础设施其实都已经现成了，真正要变的只是使用方式：不再把 PR 当成“展示最终结果”的地方，而是当成“公开协作生成过程”的地方。
PR 的重构：从“检视结果”到“协作生成” 在 Open Prompt 的范式下，PR 不再是开发完成后的检视窗口，而是一个实时协作的“在线工作台”。
具体的工作流会变成这样：
空 PR 启动：我们不再从本地秘密开发开始，而是直接先开一个公开的空 PR。
公开的 Agent 会话：在 PR 页面通过 Comment 指挥 Coding Agent 写代码。每个 PR 背后都绑定着一个长期的 Agent Session，Agent 读取指令、修改代码并自动推送到当前分支。Agent 的输出、试错和代码变更都在评论区全公开。
过程即代码：写得差不多了就把 Reviewer 拉进来。Reviewer 看到的不只是 Diff，更是整个生成代码的过程。
这里的实现甚至可以很朴素：随便找个地方跑一个长期运行的 Coding Agent Session，让它不断处理这个 PR 下面的新 Comment 并推送代码就行。再做细一点的话，可以加个 Watcher 去监听 GitHub 事件，有新的评论或变更请求时就自动把 Agent 唤醒，整个链路也就顺起来了。</description></item><item><title>记一例浮点数精度问题</title><link>http://disksing.com/floating-point-precision/</link><pubDate>Tue, 10 Feb 2026 00:00:00 +0000</pubDate><guid>http://disksing.com/floating-point-precision/</guid><description>说，内存中有两个不断递增的浮点数，我们记作 a 和 b。
程序不断运行的过程中，有两种操作会递增这两个数：
给 a 加上一个非负浮点数 给 a 和 b 同时加上一个非负整数 同时，后台有一个线程在不断向服务器以差值的形式上报 a 和 b 的增量。流程是：
deltaA = a - lastA deltaB = b - lastB report(deltaA, deltaB) lastA = a lastB = b 那么问题来了，服务端收到的 deltaA 和 deltaB 有没有可能出现 deltaA &amp;lt; deltaB 的情况呢？这里我们只考虑浮点数的精度问题，不考虑其他因素，比如线程调度等等。
显然既然写这个，答案肯定是有可能。
不过这看起来很奇怪，因为 a 和 b 的增量都是非负的，而且 a 每次的增量只会比 b 大不可能比 b 小。但是，浮点数的精度问题可能会导致这种情况发生。
问题就出在每次上报的差值运算。lastA 和 a 都是不断累加出来的，可能都有精度误差，并且误差舍入的方向可能不同，如果lastA 的误差方向是向上的，而 a 的误差方向是向下的，那么 deltaA 就可能比 deltaB 小。
直观的例子：
lastA = 2000000.</description></item><item><title>AI 辅助编程时代，哪些编程语言会更流行？</title><link>http://disksing.com/ai-era-languages/</link><pubDate>Mon, 03 Nov 2025 00:00:00 +0000</pubDate><guid>http://disksing.com/ai-era-languages/</guid><description>最近用 AI 辅助写代码的时候，我注意到一个有意思的现象：写 Go 的时候，AI 生成的代码通常很靠谱，风格也一致；但写 Python 的时候，AI 经常给出好几种写法，有时候还会搞出些奇奇怪怪的&amp;quot;聪明&amp;quot;代码。
这让我开始思考一个问题：AI 辅助编程会不会改变编程语言的流行趋势？
评价标准变了 以前我们选编程语言，主要考虑的是&amp;quot;写起来爽不爽&amp;quot;。语法糖多、表达简洁、少打几个字，这些都是加分项。Python 能一行搞定的事，凭什么要写五行 Java？
但现在情况变了。AI 帮我们写代码，&amp;ldquo;写起来爽&amp;quot;这个优势基本没了。反而，那些让人&amp;quot;写起来爽&amp;quot;的特性，可能成了 AI 的负担。
我总结了几个在 AI 时代变得更重要的语言特性。
语法糖越少越好 语法糖的本质是让程序员少打字。但 AI 不在乎多打几个字，它在乎的是&amp;quot;这段代码应该怎么写&amp;rdquo;。
Python 里实现一个简单的过滤和映射，至少有四五种写法：
1# 列表推导 2result = [x * 2 for x in items if x &amp;gt; 0] 3 4# map + filter 5result = list(map(lambda x: x * 2, filter(lambda x: x &amp;gt; 0, items))) 6 7# 传统循环 8result = [] 9for x in items: 10 if x &amp;gt; 0: 11 result.</description></item><item><title>从认知心理学看代码可读性</title><link>http://disksing.com/cognitive-code-readability/</link><pubDate>Wed, 15 Oct 2025 00:00:00 +0000</pubDate><guid>http://disksing.com/cognitive-code-readability/</guid><description>最近我发现，讨论代码可读性时，认知心理学提供了一个很好的视角。那些我们熟知的最佳实践背后都有共同的心理学基础。
代码是给人读的，而人脑处理信息的方式是有规律的。理解这些规律，就能明白为什么有些代码读起来轻松，有些代码读起来费劲；就能在面对具体问题时做出更好的判断，而不是机械地遵循教条。
认知负荷理论：理解代码复杂度的本质 认知负荷理论（Cognitive Load Theory）由 John Sweller 在 1980 年代提出，最初用于教学设计。核心观点很简单：人的工作记忆容量有限，当认知负荷超过容量时，学习和理解效率会急剧下降。
三种认知负荷 这个理论把认知负荷分成三种：**内在负荷（Intrinsic Load）**来自问题本身的固有复杂度，**外在负荷（Extraneous Load）**来自信息呈现方式不当带来的额外负担，**相关负荷（Germane Load）**来自构建和强化心智模型的过程。这三种负荷有个总的限制：内在负荷 + 外在负荷 + 相关负荷 ≤ 工作记忆容量。
应用到读代码的场景：内在负荷对应问题本身的复杂度，比如分布式事务的一致性、复杂的业务逻辑；外在负荷对应代码写得烂带来的额外负担，比如糟糕的命名、混乱的结构、不一致的风格；相关负荷对应理解代码的过程，包括理解抽象、建立心智模型、学习领域知识。
外在负荷完全可以消除，且应该尽量消除。这是我们优化代码可读性的主战场。同样的逻辑，写法不同，理解难度可能相差几倍。而相关负荷是&amp;quot;好的&amp;quot;负荷，好代码应该帮助读者高效地建立正确的心智模型。如果外在负荷太高，留给理解真正问题的容量就不够了。所以好代码的目标是最小化外在负荷，优化相关负荷。
为什么我们需要抽象 在读代码这个任务上，内在负荷并不是一成不变的。同样的需求可以有不同的设计，不同的设计带来不同的理解问题的视角。一个好的抽象能让复杂问题变简单，从而降低内在负荷。
举个例子：假设你要管理一堆相互依赖的状态变化，直接去处理这些状态转换会很复杂（高内在负荷）。但如果引入&amp;quot;状态机&amp;quot;这个概念来理解，问题就清晰多了。虽然学习&amp;quot;状态机&amp;quot;需要一些时间（相关负荷），但理解问题本身变简单了（内在负荷降低）。这就是好的抽象的价值：用相关负荷（学习抽象）来换取更低的内在负荷（问题变简单了）。
从代码可读性的角度看，这解释了为什么设计模式有价值。它们提供理解复杂问题的&amp;quot;心智工具&amp;quot;，降低问题的内在复杂度。比如 Observer 模式让&amp;quot;多对多的依赖关系&amp;quot;变得容易理解。领域驱动设计（DDD）的价值也在这里：找到合适的领域抽象，让复杂的业务逻辑从&amp;quot;一团乱麻&amp;quot;变成&amp;quot;清晰的概念组合&amp;quot;。当然，设计模式和DDD还有其他方面的价值，这里只讨论它们对代码可读性的贡献。
理解了内在负荷和相关负荷可以相互转化，我们就能明白什么是好抽象，什么是过度抽象。好抽象是降低的内在负荷大于引入的相关负荷，总认知负荷降低了。而过度抽象恰恰相反：内在负荷没降低，相关负荷却增加了，比如把简单的 5 行代码拆成 3 个函数需要跳来跳去才能理解，为了&amp;quot;可扩展性&amp;quot;引入根本用不到的复杂设计模式，为了避免&amp;quot;可能的&amp;quot;重复搞出过于通用的抽象。
工作记忆容量限制：代码可读性的核心挑战 认知心理学的一个有趣的发现是 Miller&amp;rsquo;s Law（7±2 法则），它告诉我们：人的工作记忆容量有限，一次只能处理 5-9 个信息单元。
这个工作记忆有点像 CPU 的寄存器：容量很小，但处理速度很快。而长期记忆更像硬盘：容量大，但访问速度慢。读代码时，我们主要依赖工作记忆来理解逻辑，如果工作记忆装不下，就得频繁去&amp;quot;硬盘&amp;quot;（长期记忆）里翻找之前看过的内容，效率就低了。
读代码时，我们需要在脑海中记住：变量的值、状态、分支条件、上下文等。如果超过了工作记忆容量，就会忘记前面的内容，需要回头翻看，思维流程被打断，理解效率急剧下降。
这法则可以用来解释许多常见的代码编写规范。
比如一个函数不应该太长。原因很简单：信息太多时，工作记忆不够用，读到后面就忘了前面的内容，需要回头翻看。
再比如为什么要拆分函数，为什么要封装类。它们的作用就是信息压缩，把占用多个工作记忆单元的信息压缩成 1 个信息单元，显著减少工作记忆占用。
命名为什么如此重要？当命名足够准确时，我们只要看到变量名或者函数名就能直接判断它的行为，这样可以做到完全不占用工作记忆。
不要使用过深的嵌套。这是因为在阅读过程中每进入一层嵌套时，外层的信息作为 context 需要保存在工作记忆中，嵌套过深很容易导致工作记忆不够用。而 early return 模式能很好的解决这个问题，它的价值在于：处理完某种情况后立即 return，该分支占用的记忆单元被释放，不用继续记住。
互相调用的函数要放在一起。一方面避免读到调用函数的时候，被调用的函数信息已经因为工作记忆不够用而忘记了；另一方面被调用的函数读完很快就被用掉了，可以安全地从工作记忆中释放掉，快速腾出空间。
其他的还有诸如减少变量的作用域、函数参数列表不要过长、变量的定义和使用不要离太远，都是类似的道理，不多说了。
格式塔理论 格式塔揭示了代码格式的重要性：大脑会自动把零散信息组织成有意义的整体。我们可以利用这个特性来减少工作记忆占用。
比如接近性原则：大脑会自动根据东西的间隔将它们分组。我们在编码代码时相关代码放在一起，用空行分隔不同的逻辑组。效果很明显：
1// 没分组：20 个独立信息单元 2func handleRequest(req Request) { 3 validateAuth(req) 4 validateInput(req) 5 validatePermission(req) 6 user := getUser(req.</description></item><item><title>父子对谈30则</title><link>http://disksing.com/father-son-chats/</link><pubDate>Wed, 06 Mar 2024 00:00:00 +0000</pubDate><guid>http://disksing.com/father-son-chats/</guid><description>推文合订本。
1 小朋友：蛇的英语是snake，那小蛇用英语怎么说？（注：上次跟他讲过cat/kitten，dog/puppy）
我：小蛇应该没有单独的说法，不过英语里有很多种单词都指蛇，比如 Python、Anaconda、Mamba、Cobra、Viper、Asp……
小朋友：你怎么知道这么多！
我：编程的时候顺便学的🥲
2 语重心长地对小朋友说：你要学会站在别人的角度看待问题。
小朋友：那别人如果躺着或者坐着呢？
3 想起那天坐飞机，奶奶说：你看外面的飞机翅膀好大呀！
小朋友：那个叫机翼。
4 今天给小朋友讲《愚公移山》。他总结说愚公运气太好了，正好被神仙看到。我说我要是愚公的话就不挖山了直接搬家，他说搬着家具是爬不动山的，你比愚公还笨。
5 给小朋友讲《凿壁借光》的故事，他说把墙壁凿穿不太好，应该让爸爸妈妈多赚一些钱自己买灯。
6 昨天给小朋友讲了《揠苗助长》和《守株待兔》，他的感悟分别是“要有耐心，不要着急”和“不会总是有好运气”。
7 小朋友说：爸爸看我的时候，镜子里的爸爸就在看镜子里的我；爸爸看镜子里的我的时候，镜子里的爸爸就在看我！
8 给小朋友讲成语故事。
《刻舟求剑》 我：……那人就赶紧拿刀在船边上划拉了几下做了记号记剑掉下去的位置。你看这样可以吗？ 小朋友：不可以！ 我：为什么呢？ 小朋友：是别人的船，别人没同意不能乱划。
《掩耳盗铃》 我：……那人就给自己的耳朵塞上棉花，戴上降噪耳机，啥也听不见了再去偷铃，这样可以吗？ 小朋友：不可以！ 我：为什么呢？ 小朋友：因为偷东西是不对的。
9 小朋友问：无穷大是奇数还是偶数？
10 快要跨年了，跟小朋友抒情：从明天开始就是2024年，2023年再也不会回来了，我们要跟它道别……
小朋友打断施法：除非哆啦A梦来了，还带着时光机！
11 啃甘蔗的时候突然想跟小朋友科普一下，就问：你知道炒菜用的白糖是用什么做的吗？
小朋友：甘蔗做的。
我：你怎么知道的？
小朋友：因为我们正在啃甘蔗。
我：学会揣摩出题人的意图了是吧😅
12 小朋友：整个地球最热的地方是哪里？
我：应该是泰国的曼谷。
小朋友：错啦！是地球的中间，因为都是岩浆！
13 小朋友：爸爸，游戏和真实世界是不一样的。
我：嗯？哪里不一样呢？
小朋友：游戏里面可以死很多次，只要一次没死就过关了，真实世界很多次都没死，但是死一次就再也没有了。
14 我：看，这就是导弹。
小朋友：是不给糖就捣蛋的捣蛋吗？
15 我：感恩节快到了，你最想感谢谁呢？
小朋友：最想感谢太阳。
我：为什么？？
小朋友：因为太阳是照亮世界的灯。
16 小朋友：书上说空间站的速度是7km/s，它跑100km需要多长时间？
我：14秒多。
小朋友：跑1000km呢？
我：2分钟20几秒。
小朋友：真快啊，上次你开车1000km开了两天才到。
我：是啊。
小朋友：那它跑1光年需要多长时间？
我：4万多年。
小朋友（震惊）：1光年这么远啊！</description></item><item><title>都2024了，你还没随身带录音笔吗？</title><link>http://disksing.com/life-recording/</link><pubDate>Fri, 29 Dec 2023 00:00:00 +0000</pubDate><guid>http://disksing.com/life-recording/</guid><description>2023年我最满意的数码产品就是索尼ICD-TX660录音笔，我几乎每天使用它的时间都在15小时以上，它已然成为了我生活的一部分。
ICD-TX660
为什么要录音 一开始，我的需求源于一次惨痛的经历——离婚过程中，我被前妻用捏造的证据陷害。这给我敲响了警钟，让我开始逐渐意识到保存生活中的数字资料的重要性。生活中的每一次对话，都可能成为日后的关键证据。
经历过离婚诉讼之后，我在生活习惯上最大的改进就是每天携带录音笔，早上一起床就打开录音，一直到晚上睡觉前摘下来充电，每天产生约1GB数据，我说过的每一句话都有据可查。
&amp;mdash; 象牙山刘能 (@disksing) March 1, 2023 这就是带录音笔进行全天候录音的最主要作用了：保留证据，规避法律风险和纠纷。无论是在商业交易中确认谈话内容，还是在私人生活中避免被误解或诬告，录音都提供了一个强有力的保障，可以作为证据的备份，减少不必要的麻烦。
可能有人会觉得，这样做太过极端，法庭和诉讼离自己很远，自己在生活中也没遇到过什么恶人，似乎并不值得这样做。但是，当我自己开始这样做之后，我发现每天看到的社会新闻有好大一部分，只要身上有录音笔在录着音，都是可以避免的，比如：
女性被同事性骚扰，但是没有证据，只能忍气吞声； 男性被女性诬告性骚扰，但是也没法证明自己没有骚扰； 约个炮，结果被对方诬告强奸； 被强奸了，结果对方说是自愿的； 借了钱不还； 装修谈好了价格，后面工人坐地起价； 路上扶了个老太太被赖上； 交通事故，对方承认是自己的责任，但是后面又反悔了； 等等等等…… 当然，记录事实真相的方式不止一种，比如可以随身带执法记录仪录视频，不过这种设备体积较大，而且在很多场合并不方便使用。而录音笔则可以随身携带，不会引起他人的注意，也不会对自己的生活造成太大影响。
录音笔技术上非常成熟，一般的录音笔都能提供非常清晰的录音效果，而且续航能力也很强，好一点的录音笔可以满足全天候录音的需求。从取证的角度来说，大多数情况下也是足够的。
你可能会想，为什么一定要连续全天录音，为什么不在需要的时候才录音？因为很多时候，关键信息的出现是无法预料的，你发现要录音的时候，可能已经错过了，或者才发现没带录音笔，或者发现录音笔没电了。如果全天候录音成为一种习惯，意味着你无需担心错过任何重要的时刻。
另外，每天早上开机放进口袋，晚上充电关机，有助于形成习惯。而且全天不用管，实际更无感，也会更少地干扰日常生活。
关于法律问题：录音是否能当证据使用？ 录音是否可以作为证据，在很大程度上取决于录音方式和内容。以下是一些基本的法律考量：
并非隐蔽的录音都是非法的：如果录音设备带在自己身上，且录音是在公共场所或允许录音的私人场合进行，通常不属于违法行为。所谓“窃听”，指的是你人不在现场，把录音笔放在别人的房间里或者车里，这肯定是违法的。
证明无罪：如果你面临的指控比侵犯隐私更严重，且录音能证明你的清白，那么这样的录音在法律上往往是被允许的，至少可以帮助减轻你的罪行。
法庭裁决：即便录音在法庭上被判定为非法获取，它的内容也可能作为判决的参考，因为我国司法实践在很多情况下更关心事实真相而非形式程序。
全天候录音的用处 当我开始实践全天候录音之后，我发现它的用途远不止于拿来当证据给自己辩护，它还有很多其他的用处：
强大的安全感：我甚至认为这点才是全天候录音真正最有用的地方，即使录完的音频文件一次都没打开播放过，“我录了音的”这件事儿本身就能无形中成功消除对生活中很多潜在风险的担忧。有了录音的保护，我在很大程度上不用担心陷害和抹黑，不用担心被骗被害，这实际上让我可以更有信心地去选择信任别人。
扩展短期记忆力： 我们每天接收大量信息，不可能记住每一件事。全天录音可以帮助我们回溯重要的对话，刚看过的电影的精彩台词、医生交待的用药方法、上司安排的临时任务，甚至是朋友间的争执，都能通过录音回溯。
长期备忘，如同数字日记： 通过录音，我们可以记录生活的点滴，这对于回顾生活事件有极大帮助。
人生数字化： 录音的数字化信息，在未来可以利用AI技术进行分析提取，甚至有可能用来训练AI虚拟人。
索尼 ICD-TX660 录音笔的使用体验 下面让我谈谈这款录音笔的具体表现：
便携性：它小巧轻便，非常适合长时间携带。我通常将它挂在衬衫口袋里，不穿衬衫的时候放在裤兜里，或甚至直接放在背包里都没有问题。
录音质量：大法品质，没得说，音质清晰，即使是在背包中录音，房间内的对话也能被清楚地捕捉到。
续航能力：我日常使用128kbps双声道录音，续航可以超过18小时。这个时间长度保证了我不需要在白天中途充电。一年使用下来，续航可能有些下降（没仔细测过），但还是稳稳地保证18小时录音。实际上我觉得这是一个非常“甜”的续航时间，正好白天录音睡觉的时候插上充电，假如续航加到2-3天，反而可能很容易忘记充电。
存储容量：16GB的内存略有点小，按我的使用方法（128kbps、每天16小时），每天生成的尺寸是1G左右，每半个月就会满，需要记得转存。每年的录音文件就是350G的样子，需要准备NAS或移动硬盘来备份。
注意事项：它不防水。有X友反馈一旦进水，可能会出现续航减少和按键失灵等问题。因此使用时要注意防水。
如果想买其他品牌的录音笔，也可以参考我这里提到的这些点来评估。当然了具体情况可能根据使用习惯有所不同，比如使用更高音质的录音模式会减少续航并增加存储空间占用，使用更低的音质同理。还有也可以选择晚上充电时不停止录音，可以记录下鼾声和梦话，这样存储空间也会消耗更快。
最后是广告时间，如果你觉得心动想购买我使用的同款录音笔的话，可以使用我的京东推广链接 https://u.jd.com/Ju6gcuo ，我会得到一点佣金，感谢支持。</description></item><item><title>写段子的窍门</title><link>http://disksing.com/writing-jokes/</link><pubDate>Fri, 08 Sep 2023 00:00:00 +0000</pubDate><guid>http://disksing.com/writing-jokes/</guid><description>段子之所以成为段子，最显著的结构特点就是要有转折，也被称作“预期落空”，即通过误导等叙事技巧先给读者建立一个预期，随后安排转折使预期落空。这是一个段子的趣味性的最主要来源，毕竟人人都喜欢反差感。
段子的创作过程常常是跟阅读顺序是反着的，也就是说，段子创作的起点是往往是转折之后的部分。这可以是一个笑点，比如最常用的谐音、双关、或者生活中偶然发现的有趣或荒诞的场景，也可以是自己想表达某个观点，或者自己想抒发某种情绪。
然后我们需要从要表达的点开始逆向往前扩展。拿谐音和双关来说，可以构造一个由于谐音或双关产生误会的场景，引导读者误以为是这个意思，然后在结尾揭示另外一层含义。
如果是想表达某个观点，或者评说某个事物。《喜剧圣经》有一个经典方法来让观点变得生动有趣，即考虑四个问题：为什么困难？为什么愚蠢？为什么奇怪？为什么害怕？很多严肃的主题套入这四个问题都会得到荒诞或有趣的答案。
关于如何去建立预期和让预期落空，最核心的机制就是文本的可多重解读性。这有点像赵本山小品里常见的场景，两个角色之间存在误会，然后一个角色说的每一句话都会被另一个人解读成别的意思。段子也是类似的，只不过读者不是上帝视角，而是被设计误导的另一个角色。
从日常生活中发掘可多重解读的文本是需要一定天赋的，本质上是要能从多个（可能不相关的）主题中寻找连接，而且这种连接是多多益善的——连接点越多，误导效果就越强，而且这种加强不是相加而更像是相乘的效果。
现在有一个好消息就是ChatGPT的文本联想能力相当强悍，我就经常问它一些类似“运营一家公司和处理家庭关系有哪些相似之处”的问题来寻找写段子的灵感。
与单口喜剧（国内也叫脱口秀）不同，段子是文本形式的，很容易被读者重复阅读，因此段子更适合设置理解门槛、保留一定的解读空间，让读者有一种解谜的快感。例如可以使用反讽和夸张的手法，读者可能第一感觉是这说的也太扯了，然后再回头重看一遍就理解了你真正想表达的观点。
同样因为可以重复阅读，段子不像单口喜剧一样铺垫和包袱不能相隔太远（不然听到后面已经忘了前面），在铺垫部分不管有多少货都可以无限往上叠加，最后完成转折后读者自然会回过头去重新看。
下面是操练环节，我就恬不知耻地选择我写过的几条段子，从创作过程的角度分析一下。
1. 3D打印的难题 段子的来源是在家折腾3D打印，最近接连出现打印过程中模型坍塌的情况，十分苦恼之余想到这跟芯片生产过程中良品率过低的困境有相似之处，这是一个有趣的点。
怎么去构建误导也挺显然的：让读者看起来我在说芯片生产（或者广义上的工业生产），实际上我是在说自家的3D打印。为了加强误导的力度，需要找到两者更多的相似之处（主要是“为什么困难”，此处借助ChatGPT），然后就有了这样一个简单的段子：
生产线代际落后，操作复杂，产能低，原料成本高，买不起高端设计软件，缺少专业人才，研发周期漫长，良品率过低，污染环境，这些是我在家实践3D打印遇到的主要难题。
当然，某种程度上读者也可以解读成对国产芯片或者高端制造业前景的担忧，这其实超出了这个段子的原始写作构思，不过也无所谓。
2. 福岛排放核废水事件 我想表达观点：对核废水危害的恐怖渲染是另有用心的政治宣传。不过我要直接这么发也太过于严肃无趣。那么怎么让它有趣起来呢？可以用之前提到的四个问题。
比如说这事儿为什么愚蠢？那我就想，假如今后跟日本的关系修好了，那么是不是要反过来宣传核废水无害呢？嗯，这听上去很荒诞，而且中日关系确实一直在忽冷忽热，再加上核废水的预计排放周期长达30年，确实挺有这个可能的。
按这个思路，可以这么写：
降低福岛核废水毒性的最低成本方案：日本首相就侵华问题向中国下跪道歉。
也是不错的，不过误导效果不太强，我实际最终发布的版本是这样的：
日本向大海排放核污水这事的最大风险点在于30年的排放周期实在太长了，长到足够中日关系冷热交替好几轮。
在铺垫阶段向读者暗示自己对核污染的担忧（并且使用小粉红喜欢强调的”核污水“而不是”核废水“来加深错觉），读者会误以为我想说长时间排放的技术障碍或工程管理难题，然后在第二句话锋一转，担忧的实际是中日关系的冷热交替，这样就揭示了最初想表达的主题。产生误导效果的关键在于“风险点”，一开始会被理解成核污染的风险，回过头看又可以被解读成政治宣传的风险。
3. dance / 单测 偶然发现单测的拼音dance恰好是一个单词，这是挺有趣的，但是不构成一个段子。很显然，这个素材本身就有可多重解读性，那么可以构造一个产生误会的场景，比如：
曾经有一个同事总在代码里写注释 // TODO: dance，一直好奇他是不是很爱跳舞，后来才知道他是提醒自己加单测。
这个版本可以微调一下增加解谜趣味性，不直接把“单测”这层意思写出来，比如最后一句改成“后来才知道他写的是拼音”。
我实际发布的版本是套用了“大师对话模板”，顺便说一下，有很多众所周知的笑话模板，在英语里也有经典的 knock knock、walks into a bar 等模板，套用模板的作用是能快速让读者找到熟悉的感觉，并产生一种“让我看看你还能玩出什么花样”的期待。
初学者在微信上请教大师：有了Copilot写代码速度太快了，一周的任务我一上午就把代码撸完了，接下来干点啥好呢？
大师：dance
初学者：大师我悟了，您的意思是应该放下工作去享受生活吧。
大师：shurufa huaile, jia dance
里面的几个细节：“在微信上请教”是对后面“输入法坏了”场景的铺垫；没有提到“单测”，也没提到“拼音”，只有一串拼音，增加了解谜乐趣；Copilot在这个笑话里是不必要的，提Copilot是蹭热点为了让读者有兴趣读下去。
就讲这些吧，还有一些不错的段子有点反动，这里就不说了哈哈。欢迎关注我的推特一起整活儿找乐子！</description></item><item><title>给TiDB（MySQL）写一个代理网关</title><link>http://disksing.com/tidb-gateway/</link><pubDate>Wed, 24 Aug 2022 00:00:00 +0000</pubDate><guid>http://disksing.com/tidb-gateway/</guid><description>转到cloud团队（主要做TiDB Cloud DevTier）后，这几个月大部分时间都在tidb-gateway这么个项目上折腾。现在一期功能算是上线了，准备开始做二期，趁这个机会简单总结一下。
因为TiDB是兼容MySQL协议的，所以主要其实就是折腾MySQL协议，然后如果你想做一个MySQL Gateway，大部分内容应该也是兼容的。我把相关代码整理了一下放在 tidb-gateway项目。
项目背景 先简单说下为什么需要做网关。由于TiDB Cloud是没有多租户或者serverless支持的，这就是说，用户每创建一个集群（包括免费的DevTier），我们在后台就会真给创建一个独立的集群。
跟友商的serverless方案相比，我们这么做的好处大概就是开发速度快，不用为上云做特别的改动，然后缺点就是贵。
为了降低成本，我们在DevTier上做了一定的“体验降级”：当用户的集群在连续一段时间不使用之后，我们会保存数据并把集群休眠，下次用户再需要使用的时候，需要先进行一个手动唤醒操作。
这么做完了有一定效果，至少对于已经“跑路”的集群，我们不用无限期地付出成本了。但是要继续优化，我们遇到两个障碍：
由于MySQL协议的限制，客户端通过TCP连接到服务器后，是由服务器首先发送第一个消息，同时因为是裸TCP连接，不像HTTP有请求Header可以知道客户访问的域名来进行路由。这样我们不得不为每个用户集群创建独立公网LB——据说这个还挺贵的。
临时关停的集群需要在网站上手动开启，用户体验负分，这导致我们权衡之下只针对静默7天以上（基本判定是跑路了）的集群做休眠处理，这样对每个集群都额外付出了7天的成本。
解决这两个问题的方法自然就是在TiDB前端引入一个网关服务了。
网关负责接受客户端连接并与之交换消息，等拿到用户信息之后，以代理的方式去连接真正的用户集群。同时，如果用户集群处于休眠状态，网关可以把连接阻塞，然后通知K8s唤醒，这样一来用户在休眠后第一次连接时等待一段时间，不需要在网页端做额外操作了。
MySQL建立连接过程 我们先简单分析一下MySQL的连接建立流程。
客户端向服务端建立TCP连接。
服务端返回InitialHandshake消息，其中包括版本号和一些兼容性标记（比如是否支持TLS等）
客户端返回HandshakeResponse消息，其中包括兼容性标记、连接使用的用户名及数据库名。
服务端和客户端根据AuthMethod交换若干次消息，直到服务端返回Ok或者Err消息，说明连接成功建立或者失败。
TLS连接建立过程 如果需要启用安全连接，步骤3中，Client会先发送半个HandshakeResponse消息包，其中携带了ClientSSL标记，服务端读到此标记后，会发起将TCP连接升级为TLS连接，升级完成后，Client会再次发送HandshakeResponse消息回归到常规流程。
鉴权FastPath及AuthMethod磋商 为了减少建立连接过程种消息交换的次数，MySQL Protocol有一个鉴权的快速通道。
在服务端发送InitialHandshake消息时，会先默认猜一个AuthMethod，并随机生成8字节或者更长的challenge payload，放在InitialHandshake消息中一起发给客户端。（为什么说是“猜”呢，因为不同用户可能设置不同的AuthMethod，然而在这一阶段，服务端还不知道要连接的用户是哪一个，自然不知道正确的AuthMethod应该是什么了）
客户端根据AuthMethod定义的方法对密码+payload加以计算，计算结果连同AuthMethod一起放在HandshakeResponse里一起发给服务端。
如果服务端读取对应的用户表之后，发现AuthMethod跟猜测的一致，那么就可以直接验证客户端的计算结果了，成功后直接返回Ok，这样就完成连接建立了。否则，服务端需要发送AuthMethodSwitchRequest来重新进行鉴权。
tidb-gateway的实现 Gateway的实现基本上就是经典的man-in-the-middle，在客户端和后端TiDB之间相互转发消息，顺便在中间做一些手脚。不过，首先需要解决的问题是，怎么获得连接对应的是哪个用户集群来进行路由。
传递cluster id 对客户端来说，它仍然是以连接MySQL Server的方式在连接Gateway，所以我们需要想办法在协议中插入集群信息。
MySQL的HandshakeResponse中有个Attrs字段可以用来插入一些自定义信息，可惜不是所有的DB Driver都支持设置。权衡之下，我们最后决定直接把集群id跟用户id拼接在一起，比如默认的root用户改成{clusterid}.root，这样虽然看上去有点怪，但是能保证兼容所有的客户端。
连接建立过程 这个过程比较显然了：
客户端向Gateway建立TCP连接。
Gateway构造一个默认的InitialHandshake消息返回给客户端。
客户端发送HandshakeResponse消息给Gateway。
Gateway解开HandshakeResponse，如果设置了ClientSSL此处将连接升级成TLS连接。
Gateway根据UserName设置的clusterid找到用户集群发起TCP连接，此处如果集群处于休眠状态要先唤醒。
TiDB向Gateway发送InitialHandshake。
Gateway把从客户端收到的HandshakeResponse发送给TiDB。
Gateway把两个连接串连起来对拷数据。
AuthMethod的特殊处理 由于MySQL协议中鉴权FastPath的存在，这个过程是有问题的：客户端收到的challenge payload是一开始由Gateway生成的，它跟后端TiDB发给Gateway的显然不一致，这将导致后端TiDB在收到HandshakeResponse后校验失败报错。
不过，校验失败的前提条件是FastPath被成功激活，即TiDB初始猜测的AuthMethod是正确的，否则TiDB不会激活FastPath，而是发送AuthMethodSwitchRequest尝试重新鉴权。
解决这个问题的方法也很简单，我们把转发给TiDB的HandshakeResponse篡改一下，改成一个TiDB不认识的AuthMehod，这样FastPath就不会激活了。
TLS的特殊处理 因为Gatway和TiDB的连接是在足够安全的内网，从节约能源的角度考虑，我们希望避免在Gateway和TiDB使用安全连接。
这样就带来一些问题：在客户端看来，它跟服务器之间是安全连接，但是在TiDB看来，连接是非安全的，会产生一些不一致的现象。比如require_secure_transport功能（这个选项限制TiDB只接受安全连接）就不能用了，还有系统表中Ssl相关的信息显示也都不正常。
解决办法是利用了MySQL Protocol的那个可以在插入自定义Attrs的功能，由Gateway把客户端连接的TLS相关信息通过Attrs发送给TiDB，然后我们给TiDB打了个小补丁，让它可以把TLS信息解析出来，并设置上安全连接的标记。
数据压缩和sequence number MySQL协议支持设置数据压缩，可以在进行导入导出等场景下显著节约流量。与TLS类似的，我们也希望数据压缩只在客户端和Gateway之间启用，Gateway和TiDB之间保持关闭以减少TiDB的CPU消耗。
不过，MySQL Protocol中有一个sequence number的概念，它需要被携带在每个消息包中，并且在一次客户端服务器交互过程中保持+1递增。譬如，客户端向服务器发送一个查询，拆分成2个消息包，sequence number就分别是0、1，服务器返回2次result，拆分成3个消息外，sequence number分别是2、3、4，然后客户端发送下一轮查询，再从0开始重新计数。
当Gateway两端的压缩方式不一致时，拆分包的粒度不一样，会产生sequence number对不上的情况。所以这种情况下，就不能简单地做data stream拷贝了，而是要认认真真把每个消息包解出来，并在两端分别维护sequence number。</description></item><item><title>论怎么与基层干部打成一片</title><link>http://disksing.com/country-story/</link><pubDate>Fri, 01 Apr 2022 00:00:00 +0000</pubDate><guid>http://disksing.com/country-story/</guid><description>下午发了条推特，讲与基层干部的斗争经验，看起来感兴趣的网友还不少。
谈一下与小地方基层干部的斗争经验。他们最忌惮的主要3点：怕上级，怕舆情，怕有背景的人。
所以我们遇事绝对不能怂，讲道理讲不明白就直接问对方哪个部门的，上级是谁，让他相信你有可能把事情往上捅，尽量说普通话，有单反相机的话可以挂身上，平时背几句党员的群众路线啥的备着，保准势如破竹。
&amp;mdash; 象牙山刘能 (@disksing) April 1, 2022 干脆借着兴致简单讲两个小故事，就图一乐，大家别当真哈哈。
一 大约六七年前，休假在老家，一个不知道四线还是五线的中部小城。
起因是我姨妈拿着5万块去银行存钱，结果不知道怎么被忽悠了，现场拿钱买了一个什么理财性质的保险。
反应过来不干了，寻死觅活的，结果人家也不给退。
我接到电话后，顺手拿上单反就过去了。具体为啥要带上单反，我也没细想，可能是想万一起冲突了能拿来录个像啥的。
到场之后先安抚好姨妈的情绪，然后肯定是要找他们现场最大的领导，几个人进了一个会议室协商。
有个人沉不住气了，开始旁敲侧击地问我是干什么工作的，时不时还打量我的胸前的大照相机。
哈哈，这样我可就入戏了。
我也不说自己是干啥的，装作讳莫如深的样子，但是话里话外就当作自己是一个调查记者！
压根不提我姨妈的事儿，我就只关心为什么银行里会有人在卖保险、卖保险的人是谁给放进来的、顾客是不是能清楚地知道他们是两家机构、这种做法是不是一种普遍现象、像我姨妈这样的案例是不是有很多……
我当时大概是一副过两天就准备把他们捅到《焦点访谈》的样子。
反正后来感觉那个大领导吓得够呛，我姨妈的事儿自然也就解决了。由于保险第二天才能退，他甚至主动拿自己的卡取了5万块先垫付给我姨妈。第二天我姨妈又过去了一趟，退了保险才把钱还给了他。
二 2020年初春，在我老婆娘家村里，疫情的原因封村了。
封了有大约一个月以后，因为家里有1岁的小朋友，当时纸尿裤也快用完了，也很久没吃上肉了，听人说可以去村委会开临时通行证出去采购，我就赶紧去了。
显然不可能那么顺利。
村干部趾高气昂地给我一顿教育。当然都是些老生常谈了，什么人人都过来开通行证工作没法开展啦，领导干部们也都很艰苦啦，还说为了防疫大局，只要饿不死就在家里好好呆着。
当时我也是上头了，掏出手机，开录！
镜头对着我们的大领导，我配画外音：“我现在在xx市xx村……”，然后渲染下条件艰苦生活困难，再引用一下中央说要保证人民生命安全和身体健康的最高指示，重点批判一下”只要饿不死就不管“的说法。
分分钟上微博热搜的姿势。
然后村干部明显怂了……只说让我别拍，给我发通行证就是了。
依稀记得我还补了下刀，漫不经心地问我们这村是属于哪个乡镇的，上面的领导是谁什么的。
实话说，这事儿的处理其实是冲动了，疫情笼罩之下情绪太大，不值得学习，毕竟我老婆他们家在那个村里，抬头不见低头见的，真闹僵了不好。
事情的后续是过了快一星期之后，我老丈人跟我说，他们村支书还在打听我在北京是在什么单位，干什么工作的……</description></item><item><title>怎么做一个匿名论坛</title><link>http://disksing.com/anonymous-forum/</link><pubDate>Mon, 21 Mar 2022 00:00:00 +0000</pubDate><guid>http://disksing.com/anonymous-forum/</guid><description>支持使用匿名的方式表达对公司各项政策的意见，是我司的一项光荣传统，然而在具体操作过程中，也出了一些问题和波折。
最初很长一段时间我们使用的是Slido服务，这个网站的本意是用来在公开演讲的时候观众向主持人提问的，我们发现用它来做一个匿名论坛也是不错的。
随着公司发展，人数逐渐变多，slido的一些问题也暴露出来了。最为明显的是它是没有注册的，用户只需要自己填写一个id就可以穿上马甲进论坛了，因为id信息只记录在cookie中，我们可以简单地通过浏览隐身窗口给自己套上多个马甲，这样随便一个有心人就能搞出声势浩大的样子。显然，它保证不了“一人一票”这个最基本的民主诉求。另外还有一个巨大的风险：万一出现诽谤诬陷等涉及违法犯罪的消息，我们是没有任何兜底方法去把对应的人找出来的。
后来我们的办公套件用上了先进的飞书，然后匿名论坛也换成使用飞书自带的“公司圈”，它使用了比较经典的“前台匿名，后台实名”模式，即每个人的身份和马甲有一一对应的关系，只不过这个对应关系没有人有权查看，除非出现涉及违法等少数特殊情况。
不得不说，这种模式很好地解决了slido的两个主要缺点。但它也并非完美，抛开“我们是否能信任大厂和大厂员工的职业操守”这种根基性问题不谈，实践中因为实名与匿名的对应关系实际遍布在服务器进程的整个内存空间，需要通过精细的业务逻辑控制不让这层对应关系在前台泄漏，其实很容易一个bug就直接交待了。
这里以我自己发现的一个bug为例来试说明严格保持身份信息不暴露的难度：当用户给匿名论坛中发布的实名评论点赞，并且将评论点赞成热门评论，此时热门评论里会以实名信息显示点赞列表，同时实名评论的作者收到的点赞通知内的用户名是匿名的，于是整个点赞列表的身份信息就全暴露了。
上面的例子还隐含了一个相对隐晦的问题：不同用户所能看到的界面和掌握的信息不一样，身份已经暴露的用户可能完全不自知。这里可以展开再举个例子，所有人都知道即使是论坛管理员也看不到用户身份信息，但是大家不知道的是管理员是可以在后台看到所有的发贴和删贴记录的。实际上我隔三岔五就能在后台看到有人先是用实名发了一贴，然后意识到自己忘了切匿名，于是删帖并用匿名再重新发一次……只要我不主动说，他们不会意识到他们已经是纯裸奔状态了。
故事讲差不多了，我们来从头梳理一下做一个靠谱实用的匿名论坛到底应该怎么做。
1. 真匿名 相对于“前台匿名，后台实名”，真匿名即实名信息不存放在服务器或数据库中。假匿名的问题其实不只是出bug会泄密，还有比如被黑客手段拿到数据，或者DBA监守自盗，风险无处不在，最安全的就是直接没有实名信息，也就无从泄漏了。
2. 一人一账号 这个是民主的基本诉求。思考一下会发现“一人一账号”和“真匿名”是有些矛盾的，意味着至少在注册阶段，账号需要跟实名有一些联系（不能做成slido那样）。
一种比较简单直接的做法是“抓阄”：根据总人数提前创建好X个账号，打印成纸条塞到一个大布袋里，每人摸一个就完事了。
现实情况会更复杂一些，比如公司的员工列表不是一成不变的——不断有人加入有人离开。不管是入职当天发放账号，还是入职累积够一定人数后组织发放，账号的激活时间都泄漏了真实身份相关的部分信息。
可能的改进方法是每隔一段时间（比如半年），作废所有账号，来一次全员重新发放。这么做的缺点是用户在匿名状态下维护的“人设”没了延续性，体验不太好。
另一种办法是放弃严格的一人一账号，每半年所有人都可以重新申请领取账号。这么做老员工手上会有多个号，有效利用会有更大的话语权，这个看怎么理解了，可以认为是一种福利。这么做的另一个好处是，万一不小心人设崩塌了，总是有重新来过的机会。
此外要考虑“抓阄”的可操作性问题，尤其是我们公司是分布式办公的，不可能把所有人聚到一起，如果分办公室来也面临泄漏部分身份信息的问题。因此我利用所学不多的密码学，想了一种可以在网络上完成了方法，我愿称之为“赛博抓阄”。
赛博抓阄 参与抓阄的每个人自己用 rsa 生成密钥对，把公钥提交进系统。 组织方以直播的方式运行一个脚本：这个脚本在内存中生成X个rsa密钥对，公钥直接保存进匿名论坛账号数据库，私钥在内存中随机打乱顺序，然后分别使用步骤1中提供的1个公钥加密后保存进文件。因为打乱后私钥的顺序只在存在于内存中，脚本退出后就无迹可寻了。 每个人用自己的私钥解开步骤2中使用自己的公钥加密后的密钥，可以得到一个账户私钥，用于之后的匿名论坛登录。 为了提高第2步中的可信度，我们可以当场 review 代码（应该不会长），还可以现场去 aws 等平台申请一台服务器来排除环境污染的风险。
3. 特殊情况可以追查实名信息 这个是为了应对法律风险。这条规则看起来似乎跟“真匿名”的矛盾更显然，不过我们只要保证把实名信息（也就是“抓阄”记录）存放在匿名论坛系统之外就没问题了，而且为了安全，我们可以选出若干个民意代表，规定查看实名信息需要至少X人同意。
这个在密码学上也是可以做到的，使用秘密分享加密方法，把实名信息拆成N份，并且还可以设置到时候解开信息需要至少X人提供密钥。
4. 操作记录透明公开可追溯 匿名论坛可以允许有不同角色和权限，但是不同人所能观察到的信息应该是一致的，否则其根基性的身份安全将受到威胁，极端情况就是前面说过的裸奔而不自知。
另外，匿名论坛的帖子不能删除，不能修改（如果允许修改则应保存修改记录）。这个规则同样是为了保证不同人观察到的信息是一致的，不应该因为某个人在特定的时间打开了论坛就掌握了更多信息。
5. 紧急逃生通道 如果用户发现自己不小心身份暴露或者人设崩塌，可以使用一键逃生功能，停用账号并销毁自己的所有记录。这条乍看来跟上一条是有矛盾的，但是从更底层的逻辑上来说，第4条是为了避免用户身份暴露而不自知，这一条是为了用户身份暴露的情况下减少损失，两者都是为了用户能更有安全感地畅所欲言。
6. 信道安全 为了打消用户对公司内网（或远程VPN）网络监听的顾虑，匿名论坛应该部署在公开网络。这样可能会涉及到离职员工账号的问题，可以考虑给论坛多设置一个定期更新的全局密码，或者干脆定期更新地址。
总结 感觉其实不怎么难，有时间可以考虑做个原型，先挖个坑。</description></item><item><title>适合程序员的桌面窗口管理方案</title><link>http://disksing.com/desktop-layout/</link><pubDate>Tue, 15 Mar 2022 00:00:00 +0000</pubDate><guid>http://disksing.com/desktop-layout/</guid><description>介绍下我在办公室使用的桌面窗口管理方案。
先说下硬件：我的工作电脑是一台 13 英寸的 Macbook Pro，外接了一个 27 英寸的显示器，使用了一个电脑支架把笔记本架起来了，这样两个显示器差不多是并排的样子。
然后看下我的窗口布局规划，可能会比较特殊一点，不过是经过精心考虑的。
桌面布局示意
右边的大显示器我按 1:2 分成两列，左边这一列是最大的显示空间，我把它当作“主空间”来使用，一般我在编码状态下会把编辑器放在这个位置，非编码状态下这里通常就是浏览器了。因为本来两个显示器的大小是不一样的，这样进行划分，主空间两侧的空间反而是比较均衡的状态了。显示器和电脑屏幕在桌子上的摆放也是非对称的，当我坐下时，正对着的是这个“主空间”的正中央，这样也避免一个常见问题：两个一样大的显示器对称排布时，正对着的位置恰好是两个显示器中间的缝，于是工作中几乎时刻是扭着头的，时间一长脖子就受不了了。
左边笔记本的屏幕没有做切分，这块空间一般在编码的时候放浏览器看文档资料，或者放个 Terminal 调试，也可以是另一份代码，使用完整的屏幕保证它总是够用的，不至于不得不把窗口拉大然后频繁切换窗口。
大显示器右边的 1/3 被一分为二，这两块空间主要用来放IM软件，包括飞书、Telegram、微信、QQ、Twitter桌面版……IM软件也平铺出来也是为了减少切换窗口，我只需要在干正事的时候时不时瞟一眼就行了，有需要关注的消息时再去处理。必要的时候这个小格也可以临时放一下 Terminal 之类的小窗口。
再说窗口管理软件方面，可能我的搞法比较变态，我也没找到合适的软件，最后使用的方案是 HammerSpoon 一点脚本，HammerSpoon 大体上就是 Mac 版的 AHK，功能是弱了很多，不过在窗口管理这一块还是完全够用的。我的脚本很简单，使用 4 个快捷键，分别把窗口移动到 4 个格子：
1hs.hotkey.bind(&amp;#34;cmd&amp;#34;, &amp;#34;1&amp;#34;, function() 2 local sf = hs.screen.primaryScreen():frame() 3 hs.window.focusedWindow():setFrame(hs.geometry.new(sf.x, sf.y, sf.w*2/3, sf.h)) 4end) 5 6hs.hotkey.bind(&amp;#34;cmd&amp;#34;, &amp;#34;2&amp;#34;, function() 7 hs.window.focusedWindow():setFrame(hs.screen.allScreens()[2]:frame()) 8end) 9 10hs.hotkey.bind(&amp;#34;cmd&amp;#34;, &amp;#34;3&amp;#34;, function() 11 local sf = hs.screen.primaryScreen():frame() 12 hs.window.focusedWindow():setFrame(hs.geometry.new(sf.x+sf.w*2/3, sf.y, sf.w/3, sf.h/2)) 13end) 14 15hs.hotkey.bind(&amp;#34;cmd&amp;#34;, &amp;#34;4&amp;#34;, function() 16 local sf = hs.</description></item><item><title>Go语言泛型初体验</title><link>http://disksing.com/try-go-generics/</link><pubDate>Fri, 11 Mar 2022 00:00:00 +0000</pubDate><guid>http://disksing.com/try-go-generics/</guid><description>Go1.18rc1 放出来也有一段时间了，我们期待了多年了泛型的支持终于是要实装了，毕竟已经是RC，后面语法应该不会再大动了，所以决定提前来学习一下。
前几年曾经用Go语言移植了C++ STL的迭代器和算法库（disksing/iter），因为当时没有泛型，所以基本上是 interface{} 和 type assertion 满天飞的状态。这次我就用它来学习泛型，试着改个泛型版本出来。在C++里面，迭代器和算法这块可以说是泛型应用的典中典，所以我觉得要是能把它给改完，应该能说明实用程度是足够的了。
先说结论吧，我觉得这一版至少可以打85分。中间确实也遇到一些障碍和小的体验问题，但是瑕不掩瑜，它在“保持简洁”和“提供更完善的功能”间保持了非常好的平衡。几乎不需要了解什么额外的概念和实现原理，就凭着自己对泛型朴素的理解，就能比较顺利地上手了。
最简单的基础用法这里就不多说了，有兴趣的话可以参考下官方blog的那篇文章。这里仅挑我遇到的几个问题分享一下。
自指 有时候我们需要在 interface 中定义与具体类型相关的方法，比如 Copy() 用于复制一个同类型的对象，或者 Next() 用于返回指向下一个位置的迭代器，又或者 Equal() 用来和同类型的对象进行比较。
在 Rust 里面有一个 Self 来解决这个种问题。在 impl 的时候，你的具体类型是啥，就返回啥。
1trait Copyable { 2 fn copy(&amp;amp;self) -&amp;gt; Self 3} 在 Go 里面，没有泛型之前，我们一般是这么干的：
1type Copyable interface { 2 Copy() Copyable 3} 不过这不是泛型，只是一个常规的 interface。我们在实现具体 struct 的时候，Copy() 只能返回 Copyable 而不能用具体类型，在使用的时候还需要强转一下。
1type myType struct{} 2 3func (t myType) Copy() Copyable { 4 return myType{} 5} 6 7func main() { 8 x := myType{} 9 y := x.</description></item><item><title>双中心主从模式</title><link>http://disksing.com/dual-datacenter-master-slave/</link><pubDate>Sat, 11 Sep 2021 00:00:00 +0000</pubDate><guid>http://disksing.com/dual-datacenter-master-slave/</guid><description>在之前的Paxos从入门到学会Raft一文中，为了引入paxos/raft共识算法，简单地讨论了一下主从模式以及为什么主从模式不能最大限度地同时保证高可用和一致性。不过在现实场景中，常常因为基础设施不足，网络成本控制等原因，无法使用需要三中心的paxos/raft，所以我想再详细讨论下只有两中心的场景下的妥协方案。
简单回顾下上次的讨论：在两中心主从模式下，我们如果想要主从切换时不丢数据，就必须使用同步模式，即主中心写入的数据，需要同步到从中心落盘，再给客户端返回写入成功。
同步模式的困境在于，从任何一个中心的视角来看，都无法区分出“另一个中心故障”和“两中心网络断连”这两种异常情况。这导致主从模式下的failover是一定无法由程序自动进行的：
如果主中心在发现从中心故障（或断连）时自动切换至独立运行（异步）模式，那么当主中心发生故障时，从中心无法切换至独立运行模式，因为从中心无从判断主中心是真故障了，还是网络断连了（这种情况下主中心可能已切换至独立运行模式）。
反过来如果我们让从中心在发现主中心故障（或断连）时自动切换至独立运行模式，同样的道理，我们也无法处理从中心故障的情况。
这里我们面对的是一个经典的CA抉择问题，大方向上有两个选择。
第一个方向是优先保证一致性，一旦出现故障或者网络断连了就主动停止服务，由运维来选择一个中心来恢复服务。我们都知道，一旦需要人工介入，高可用性这块基本上就免谈了。还有一个可能产生麻烦的点在于在发生故障的前提下（且可能是网络故障），或许会给运维人员接入生产环境带来一定的困难，这也会进一步增加故障恢复时间。
另一个方向就是优先保证高可用。典型的做法是主中心在发现从中心故障（或断连）时，自动切换成异步模式提供服务。如前所述，一旦主中心故障，就需要人工干预来进行主备切换了。要特别注意这种情况下主备切换不仅比较tricky还有丢数据（不一致）的可能，以下两段划重点：
主备切换之前，需要先把主中心的服务给“掐掉”，因为主中心是有自动切换异步模式单独服务的机制的，如果在主备切换之后假性故障的主中心死灰复燃，两个中心同时提供服务，会产生非常严重的后果！那么怎么把主中心的服务掐掉呢？如果此时能接入主中心，可以直接停掉所有进程或者通过配置开关禁用自动切异步模式的功能；还可以通过网关、防火墙等配置断开应用和主中心数据服务的连接；另一个选项是在应用层做主备切换，保证所有应用都只连接从中心的服务。如果这些都做不到，此时做主备切换就要承担很大的风险了，建议上报给老板做决策。
主从数据可能不同步。理论上讲在同步模式下主中心发生故障，从中心一定有全量数据。但是实际上如果此时不能接入主中心检查状态，我们单看从中心无法排除这种可能性：主中心在完全故障之前，先跟从中心发生网络断连，随后自动切换异步模式并写入了一些数据。因此，做主备切换之前，除非能接入主中心并确认其没有切换过异步模式，还要承担丢数据的风险，这里同样建议您先上报老板。
工程实践中，C和A并不是非此即彼的选择题，常常有权衡的空间。在双中心主备切换的场景中，我们可以牺牲一些可用性来换一些一致性。
做法也很简单，就是给主中心切换异步模式设置一个比较大的超时时间，比如30分钟。这样当从中心故障时，主中心需要等待30分钟才能独立提供服务，牺牲了可用性。换来的一致性保证是，当主中心故障（或断连）时，如果我们在30分钟之内做主备切换，就能确定一定不丢数据。
具体的超时时间可以根据需要进行调整，很大的值其实就是一致性模式，很小的值对应的是高可用模式。看到这里熟悉Oracle的朋友们应该都会心一笑了，这其实就是DataGuard的maximize protection模式和maximize availability模式嘛。
接下来简单聊一下架构层面改进的两种思路。
第一个思路是向三中心架构推进一步。开头也说了使用paxos/raft三中心的主要问题是成本太高了，然而实际上不需要完整的三中心三副本也能达到比较好的效果。主备模式的主要缺陷在于两个数据中心的地位是完全对等的，出现网络隔离的时候无法判断对面是不是真挂了。
我们可以在第三个数据中心部署一个简单的etcd服务来扮演“仲裁者”的角色，在主从中心正常工作的时候，不需要与etcd作任何消息交换，发生故障时，哪边能连上etcd，哪边就有权切换成独立服务模式。假如主从网络断连，同时它们又同时能连接到etcd，那么主中心切换成异步模式并把状态信息通过etcd传递到从中心。
由于跟第三个数据中心之间只需要传递简单的状态信息，可以考虑使用移动网络、无线电或者卫星通信（如北斗短报文）来进一步减少成本。
另一个思路成本更低一些，不使用第三个数据中心，直接在主从中心之间建立低成本旁路通信同步状态。这时因为没有“仲裁者”了，在出现整个数据中心故障时仍然会陷入“两难”的境地，不过在发生网络故障时，主从中心可以通过旁路进行状态交换然后迅速进行异步模式切换。
最后来点主题升华哈。权衡是软件工程架构设计的最经典命题之一，绝大部分情况下都没有完美的设计，只有特定约束条件下最平衡的设计。诚然，一个又一个“不可能三角”告诉我们完美的系统是不存在的，但换个角度来看，不可逾越的鸿沟也是可以去不断逼近的极限，这就是工程师的浪漫呀！</description></item><item><title>TrueTime和原子钟</title><link>http://disksing.com/truetime/</link><pubDate>Wed, 10 Feb 2021 00:00:00 +0000</pubDate><guid>http://disksing.com/truetime/</guid><description>如果你关注分布式数据库，相信多少听说过Google的分布式数据库Spanner，以及Spanner使用原子钟搞了一套TrueTime来实现跨数据中心的分布式事务。
而Spanner的后继者们，却都采用了不使用原子钟替代方案，比如TiDB的TSO，CockroachDB的HLC。对此很多人的印象就是Google财大气粗，所以有能力搞原子钟这种精密高端设备。这个说法不能说全错，但至少不是完全准确的。
网络时钟同步 上面提到的3种取时间戳的方式的底层逻辑是迥然不同的。TiDB的TSO是中心授时，每一个时间戳都要从中心服务器获取；CockroachDB的HLC本质上是逻辑时钟，依赖于消息交换时去推进时钟计数器；Spanner的TrueTime是时钟同步，通过定期交换消息，把本地时钟与源时钟进行同步。
时钟同步的模式跟我们日常用手表的方法是类似的，我们隔一段时间把手表跟新闻联播同步一下，期间的时间直接从手表上读出来。
计算机里面最常见的时钟同步就是NTP了，通过网络同步时钟有个问题就是延迟导致的误差。
网络同步时钟
比如客户端在12:00:00发起查询请求，2秒钟后收到服务器的消息，返回的时间也是12:00:00。这时并不意味着本地时钟是准确的，因为消息发到服务器要花费时间，本地时钟实际上是快了一点。但是具体快了多少是没法知道的，我们只知道消息一来一回花了2秒，却不知道来回分别花了多长时间。因此只能大概估摸着取个中间值，把时间往回拨1秒，这时误差范围就是±1秒了。
Marzullo算法 只从单一时间源同步时间是不够靠谱的。除了有可能发生故障或者网络中断，更可怕的是时间源本身就出了问题。Marzullo算法就是用来从多个时间源来估算准确时间的算法。
Marzullo算法
如图，我们通过向ABCD四个时间源查询时间分别得到时钟偏差及误差范围，算法的大体思路就是选被尽可能多时间源所覆盖的区间（缩小误差范围），并排除掉有问题的区间（如A）。
不过，在对时序有严格要求的场景（比如分布式事务），Marzullo算法还要进行一些改良。例如比较明显的缺陷是，当有问题的时间源offset区间与正常的区间有交叠时，可能导致误差范围被估算得过小。如果想了解相关细节，可以去研究下相关资料，这里不展开了。
时钟漂移 跟服务器对上时间了还没完，通常对时的过程都要周期性地触发。正如我们的手表用着用着就不准了，CPU的晶振周期也不是完全精确的，会受温度和电压的影响，时间一长也会“跑偏”。
Spanner假设他家服务器的误差不超过每秒钟200μs。按最大值去计算，30秒不同步，误差最多会累计到6ms，如果1天不同步，最大误差达到约为17s。要注意这里的误差范围是非常非常保守的，实际情况CPU远不可能这么糟糕，举个例子对比一下，我国石英电子表的行业标准是，一类月差10-15秒，二类月差20-30秒。
原子钟 原子钟，是一种利用原子、分子能级差为基准信号来校准晶体振荡器或激光器频率，以使其输出标准频率信号的一种装置。它的工作原理是：利用原子吸收或释放能量时发出的电磁波来计时的。由于这种电磁波非常稳定，再加上利用一系列精密的仪器进行控制，原子钟的计时就可以非常准确了，可以达到千万年仅差一秒或者更好的水平。
—— 时间频率：5G 叠加自主可控， 被忽视的高精尖领域
看上去确实很高端，那么假如想买这样一个原子钟要多少钱呢？实际情况是原子钟比听上去亲民的多，我们直接在东哥的网站上就能搜到：
京东商城售卖的原子钟
售价大约是几万到十几万不等，并非承受不起的昂贵，和一台高端点的服务器是差不多的价位，如果降低精度的要求还能更便宜。
说白了原子钟和计算机上面随处可见的晶振就是同一类东西，只不过精度高了好几个数量级。
不同硬件的计时精确度
需要注意有些同学误认为TrueTime需要每台机器都要给配一个原子钟，其实不用，一个数据中心有几个就完全足够了，具体先按下不表后面再说。
GPS授时 GPS不仅提供定位服务，还可以授时。每个GPS卫星都携带了数个高精度原子钟，并不断广播星历（运行轨迹）和时间。地面装置从至少4颗卫星接收到信号后，解开以三维空间+一维时间为变量的四元方程组，就能同时拿到时间空间信息了。
GPS的精度非常之高，可以把误差控制在数纳秒以内。这是因为电磁波信号基本上是直线传播，路径上受到的干扰很小，根据距离可以很准确地计算出信号传递延时。而网络消息会受中继和多层网络层层封包的影响，而且即便在光纤中，信号也不是沿直线传播的。
TrueTime 背景知识介绍完毕，下面我们就来看看TrueTime到底是怎么做的。
机房内的TrueTime组件部署
TrueTime组件按角色分成 time master 和 time daemon。time master 可以认为是 TrueTime 的服务端，部署在一些独立的机器上，time daemon 是客户端，以进程的形式部署在每个实际运行业务的主机上。
time master 又分成两类。一类安装 GPS 模块，分散在机房的不同位置，每个GPS节点都使用独立的天线，避免因为信号干扰的原因一起失效了。另一类安装的是原子钟，原子钟也是多台来防止故障产生不可用。
各种 time master 周期性地使用Marzullo算法相互对时，每个 time daemon 也会以 30 秒为周期跟多个 time master 进行对时（同样使用Marzullo算法）。
之前介绍过，GPS 的精度是纳秒级别的，这个误差跟机房内的网络延迟比起来都可以忽略不计了，直接计作0ms。这样time daemon进行时钟同步之后的误差就仅仅取决于网络延迟了，一般机房内不超过1ms。
我们还要考虑到完成时钟同步之后，到下一次同步期间time daemon的时钟漂移，也就是前面计算过的，30秒内最大误差可能累计到6ms。于是，time daemon上的时钟误差范围就在1ms到7ms之间不断涨落，画出来是这样的锯齿状：
time daemon误差范围变化示意
那么问题来了，原子钟是干啥用的？</description></item><item><title>价值6万元的TiDB Hackathon创意</title><link>http://disksing.com/ya-hackathon-idea/</link><pubDate>Thu, 17 Dec 2020 00:00:00 +0000</pubDate><guid>http://disksing.com/ya-hackathon-idea/</guid><description>前两天发表了价值10万元的TiDB Hackathon创意，反响还不错。可惜这个项目最大的问题是投入成本过大，至今没有天使投资人出现。所以只好再发一个，这次的特点就是成本低，见效快，投入产出比高！
先说问题。
TiKV并不单纯是给TiDB用的，作为CNCF的开源项目，是Cloud Native基础设施的一块重要拼图。如果想以各种有趣的姿势来玩耍TiKV，我们首先需要有客户端来跟TiKV进行交互，这是绕不过去的。
遗憾的是，到目前为止，我们只有一个功能完备，生产环境验证的TiKV客户端——就是内嵌在TiDB里的那一个。如果要使用的话，需要引入庞大的TiDB依赖，更要命的是，只支持Go一种语言。
虽然我们有一些其他语言的port版本，比如 Java，Rust，C，不过大多数功能有缺失。而且没经过充分验证，生产环境也不太敢上。不得不说，这对社区很不友好了。
所以Hackathon项目就是搞多种语言的客户端呗？
并非如此，实际上我怀疑两天时间写一个客户端都是完成不了的，因为开发一个TiKV客户端其实很难。主要体现在：
客户端是分布式事务的协调者，需要处理大量微妙的事务逻辑 客户端需要从PD查Region元信息，并维护一部分缓存 客户端要处理各种错误和异常 需要port大量的单元测试和集成测试来保证正确性 我想做的是充分利用现有的资源，做一个TiKV客户端测试框架，让客户端的测试验证变得更容易。开发客户端的时候，不用再写测试了，这不仅可以节省工作量，而且使用标准的流程来验证客户端，可以让我们对不同实现的质量更有信心。
我们看图说话，描述一下工作原理。
客户测试框架宏伟蓝图
TestAdapter 为了接入测试框架，用户需要为开发中的客户端写一点简单的胶水层代码来跟测试框架交互，也就是TestAdapter。TestAdapter需要按照协议启动一个HTTP服务，本质上就是一个创建和调用Client的代理。
MockTiKV / MockPD 客户端的测试经常依赖于TiKV/PD的特殊状态，比如Region发生leader切换，或者Region在特定的位置分裂，或者TiKV宕机，等等。
但是我们在测试的过程中，不太可能去启动一套真正的集群，而且更不太可能去精细地控制集群的内部状态。所以目前TiDB的做法是用Go写了个假集群，也就是MockTiKV/MockPD，它们提供跟真正集群一样的gRPC服务，同时暴露一些接口来设置内部状态。
测试的启动（蓝色箭头） 开发者把TestAdapter的URI填入测试框架的网页输入框，点击开始测试。
测试框架在后台开启测试任务，并依次运行准备好的所有测试用例。
测试运行（黄色箭头） 每个测试在运行过程中会先创建Mock集群，然后把Mock集群的服务地址交给TestAdapter，创建一个或多个Client实例。随后，测试用例不断地给TestAdapter和Mock集群发消息来完成测试。
举个例子吧，比如测RawPut这个功能，过程就是先通过TestAdapter调用Client的RawPut接口，随后再调用Mock集群的RawGet接口看看是不是被写入了。
测试的过程中通过Admin API来控制Mock集群的状态，甚至通过failpoint注入一些错误。
测试报告（紫色箭头） 每个测试用例的结果返回给测试任务，汇总后生成测试报告展示在网页上。
客户端可以选择只支持部分功能，比如只支持RawKV，或者只支持2PC TxnKV。测试报告也做一个分门别类，分别指明客户端对于每种功能是通过，不支持，或者有bug。对于有bug的情况，可以提供相关的运行日志供检查。
这一套框架长期来看收益应该是很好的。在减轻客户端开发者负担的同时，最大的好处就是测试用例可以复用，当我们发现新bug后，可以做到一次添加就测试所有客户端的效果。后面我们还可以在官网上做一个大的客户端Dashboard，方便开发者进行选择。
与此同时，这个项目做起来工作量也不大。MockTiKV和MockPD可以复用之前的代码，只是要封装网络层。测试用例也可以从TiDB现有的代码移植，同样用Go语言的话移植成本也会比较低。
最后还有个彩蛋，就是这个项目其实之前我跟几个小伙伴已经做了一些微小工作了（tikv/client-validator，tikv/mock-tikv），后来由于个人原因（主要是懒）没有继续。当时的进展其实已经不错了，可以以命令行的方式运行并输出简单的报告，不过测试用例是很缺的，只有rawkv的简单功能测试。
大体上就是这样了，有兴趣的话，记得来联系我啊。</description></item><item><title>价值10万元的TiDB Hackathon创意</title><link>http://disksing.com/hackathon-idea/</link><pubDate>Tue, 15 Dec 2020 00:00:00 +0000</pubDate><guid>http://disksing.com/hackathon-idea/</guid><description>今天，PingCAP官方又高调宣布了2020年的Hackathon计划，头奖奖金高达10万，相信各路大神也是蠢蠢欲动了。其实我有个创意第一届就想做了，各种原因吧，没能搞成。今年其实也不太可能搞，主要是项目投入成本有点高，所以干脆写出来，要是有人看上的话可以联系我啊哈哈。
缘起很简单，就是想优化一下我们TiDB技术支持的体验。我们先分析一下，我们DBA在支持客户的时候，最大的痛点是什么呢？
是频繁的扩容缩容吗？ 是复杂冗长的运维操作步骤吗？ 是繁琐漫长的问题诊断吗？ 显然对于现在的TiDB来说，这些都不是事儿。
根据马斯洛需求层次理论，当基本的生理和安全需求被满足了之后，人们就需要去满足更高级别的精神级别的需求。
因此，我大胆断定，现在对于TiDB的DBA来说，运维TiDB最大的问题就是：不够酷炫，不够嗨！
所以我们来搞一些设施，让运维体验嗨皮起来！
项目的名字叫“TiDB驾驶舱”，大致就是一个座舱，要运维集群的时候就直接坐进去，大家可以想象一下下面这两张图的结合形态。
驾驶舱示意图
TiDB驾驶舱，让你运维TiDB集群有如在开歼-20的感觉！
想象一下，你坐进驾驶舱，然后一条命令连上集群。此时：
终端自动接入堡垒机 屏幕开始显示关键metrics 空速表和高度表显示的是集群的QPS和latency 油量表显示集群剩余存储空间 姿态仪显示的是集群的balance状况 闪烁的LED矩阵指示着集群里各个节点的健康状况和负载情况 各种开关根据集群配置情况初始化至对应的状态 突然，警报器发出刺耳的蜂鸣音，终端提示你：有节点发生了故障。你冷静地扫了眼LED矩阵，发现一个红灯，原来是TiKV故障了。按下对应的按钮，登录上了对应的节点，查日志发现是磁盘损坏了。
你熟练地拨动着开关，很快就做完了下线处理。最后，你观察到补副本的速度有些慢，于是你轻推节流阀，慢慢提升调度速度，同时密切注视着集群状态的变化……
是不是很嗨皮？是不是很朋克？
这还没完，我还设计了一个扩展包，可以让朋克升级成赛博朋克。
扩展包要解决的是另外一个很现实的问题。
随着TiDB的兼容性和稳定性不断提升，慢慢地也开始进入一些银行金融的核心场景。而这些客户的运维支持有一个很麻烦的问题，就是因为安全级别比较高，他们一般是不允许远程接入的，只能是去现场人肉排查问题。这也在一定程度上增加了DBA们的工作负担，降低了嗨皮度。
针对这个问题，我设计了远程oncall机器，在遇到这种情况的时候，我们就不用DBA跑去客户现场了，直接叫个闪送把oncall机器人给送过去，我们还是坐在驾驶舱，远程操纵。
我画了个示意图，大家将就看下吧……
oncall机器人示意图
说是机器人，其实也很简单了，就是一个三脚架，加上一个摄像头和一个能操作键盘鼠标的机械手，再接上能连移动网的4G模块。当然也能加一些比如语音扩展包啥的，不是事儿。
此时我们跟客户的生产环境是纯物理接触的，安全级别足够高，而且客户把机器人放在电脑面前了它就动不了了，也不怕乱跑乱看，甚至比真人过去更让人放心……
好了，大体就是这样了，我觉得这个做出这个来拿个头奖真不过分。感兴趣的话，快来联系我啊！
卖萌</description></item><item><title>TiDB1024谜题解题报告</title><link>http://disksing.com/tidb-puzzle/</link><pubDate>Sat, 24 Oct 2020 00:00:00 +0000</pubDate><guid>http://disksing.com/tidb-puzzle/</guid><description>今天（10月24日）被大家称为程序员节，PingCAP也凑热闹发布了一个谜题。其实是很简单，不过借助这题，我也是学习了一些 linux 命令的使用方法，还是很有意义的。不想关注公众号的话，题目也能在asktug直接看到。
首先谜面看形式像是摩尔斯电码，但肯定不是，因为摩尔斯电码不是定长的，但是这里每组长度都是8，大概率是ASCII。
这里-和.想必是0和1（所以提示的莱布尼茨是二进制的意思？），又因为ASCII第一位总是0，所以我们把-换成0，.换成1，再按ASCII转成拉丁字母。
先用 tr 做简单的字符替换，同时把空格换成空行方便后续处理。
cat CODE | tr &amp;#39;-&amp;#39; &amp;#39;0&amp;#39; | tr &amp;#39;.&amp;#39; &amp;#39;1&amp;#39; | tr &amp;#39; &amp;#39; &amp;#39;\n&amp;#39; 输出是
00100000 01110101 00110101 00110100 00111000 ... 接下来我们用bc来做个进制转换，方法是设置ibase和obase：echo &amp;quot;ibase=2;obase=10000;00100000&amp;quot; | bc。注意这里我们先设置ibase=2，接着设置obase的时候要按ibase的格式，也就是二进制的16（10000）。
在把之前的结果交给bc之前，先用sed整理下格式。
sed &amp;#34;s/^/ibase=2;obase=10000;/g&amp;#34; | bc 输出：
20 75 35 34 38 ... 然后我们用xxd工具，这个一般是用来做hex dump的，不过给它加上-r参数后，可以把hex形式给转回去。在hex之前，我们要整理下格式，把换行符删掉。
tr -d &amp;#39;\n&amp;#39; | xxd -p -r 输出：
u548c u0020 u0054 u0069 u0044 u0042 ... 看上去是固定的u后跟4个字符，显然是unicode了。我随便找了个在线转unicode的网站，发现它要的输入格式是\uXXX。这好说，再用tr把空格换成\，然后填到网站上就出结果了：
和 TiDB 一起用代码.... 看上去这就是答案了，用一行脚本的话就是：
cat CODE | tr &amp;#39;-&amp;#39; &amp;#39;0&amp;#39; | tr &amp;#39;.</description></item><item><title>Paxos从入门到学会Raft</title><link>http://disksing.com/paxos/</link><pubDate>Tue, 20 Oct 2020 00:00:00 +0000</pubDate><guid>http://disksing.com/paxos/</guid><description>我觉得学习Paxos/Raft的最大障碍并不是算法本身复杂，而是难以理解。就好像某些数学结论，证明过程不难，但是结论却很难从直观上去理解。本文就是希望能借助一个假想中的系统，逐步加强约束，引导到Paxos/Raft，希望能一定程度上解释“为啥要用共识算法”以及“不用共识算法会怎样”的问题。
本文结构在很大程度上参考了drdrxp阁下的一个PPT，他的微博主页上也有对应一篇很棒的关于Paxos的文章，这里表示感谢及一并推荐给大家。不过因为理解角度不同，本文很多地方有诸多差异，例如关于半同步复制为什么不可行，本文给出了另一种解释，另外这里没有讲Fast Paxos，但是多了关于Raft的内容，希望读者可以进行比较阅读：）
单机 我们假想一个抢购手机的网络服务，因为这款手机的用户都比较发烧，所以一次只卖一个手机。在活动之前，系统会给每个用户分配一个E码作为唯一标识，抢购时间到达之后，所有用户通过客户端发送E码到服务器，服务器把手机分配给一个用户。
第一版设计我们使用单机服务器模式：搞一台主机作为服务器，当收到第一个请求后，保存这个用户的E码，并给客户端返回“抢购成功”，对于后续的所有请求，只要E码跟保存的不一样，一律返回“抢购失败”。
单机模式的缺陷大家都耳熟能详了，就是不能容忍节点发生故障。仅有的一台服务一旦故障，整个服务就不能用了，这个指的是可用性。还有一个批判的角度是容灾性，如果这台服务器的数据损坏了，我们将无从判断这台手机是否已经被卖给了某个用户。
备份（异步复制） 大家都知道用户数据是非常重要的资产，万万不能丢，一定要备份。
所谓备份，就是定期把数据拷贝一份放在别的地方。还有一个概念叫异步复制，其实本质上差别不大，我们放在一起讨论。这里说异步，指的是最新的数据并不是与备份副本实时同步的。
备份能解决一部分数据容灾的问题。这里限定说“一部分”，是因为异步模式存在一个不同步的时间窗口。如果Master在(3)OK返回给客户端之后故障了，E的值将不能被复制到Slave。之后如果使用Slave数据来恢复服务，手机将再次被卖给另外一个人，也就是一致性被破坏了。
同步复制 异步的不行，那同步的怎么样呢？
如图所示，Master收到请求后，先同步给Slave，Slave存盘后返回OK，然后Master再存盘并给客户端返回OK。
如果Slave故障了，我们把Master切换成单机模式继续提供服务。如果Master故障了，我们就把Slave切换成Master提供服务。因为是同步的，两种情况都不会产生数据丢失。
注意这里假设Slave在存完盘返回消息之前故障，也不算丢数据，因为此时Master并没有给客户端返回OK，所以手机是可以再卖给另一个人的，只需要在Slave恢复之后，Master再把新值同步过去就行了。
看上去就很完美了，可用性和一致性都能得到保证，只需要有一个负责任的工程师来盯着服务器，故障的时候切一下状态就行了。
问题就出在这个工程师身上。
我们必须要把工程师这个人也算成分布式系统的一部分，要考虑到人也会故障的（生病，意外，手机欠费失联，突然想去看看世界），而且通常管理员也是通过网络来运维管理，当服务器节点之前网络中断时，管理员也很可能无法访问某些节点。实际上我们完全可以把工程师看作集群里的一个故障检测程序来分析问题。
如图(A)，假如Admin节点离Master比较近，那么当他们一起故障时，Slave无法被提升成Master。同理(图B)，Admin跟Slave一起故障时，Master也无法切换成单机模式。
那我多搞几个Admin，分别跟Master/Slave部署在一起行不行？也是不行的，这样看起来Master/Slave不管谁故障了，另一个没故障的总有Admin来操作。但是假如发生了网络隔离，如果Admin判断对面故障了，贸然切换状态，可能会出现两都是Master同时提供服务，一致性被破坏。
还有一种打补丁的思路，就是引入一个仲裁者(图D)的角色，Master和Slave不断心跳上报状态，发现对面失联想切换状态时，也要向Meta申请。这样一来，当Master和Slave断开时，取决于谁跟Meta是连着的，以及谁能更快地把状态切换请求发给Meta。
不过这里的问题在于，如何保证Meta的高可用和容灾性呢？（禁止套娃）
半同步复制 回顾一下上面提到的各种方案，我们能发现一个有趣的现象：每次都是跪在系统中的特殊节点上面。比如仲裁者Meta，或者负责切换状态的Admin，还可以包括单机模式下的那个唯一单点。由于特殊节点的不可替代性，一旦故障了，牵一发动全身，整个系统就离挂掉不远了。
说明一下，这里从可用性来分析，我们不认为Master是特殊节点，因为Master和Slave是可以相互替代的。
从消除特殊节点的思路出发，我们把之前方案里的仲裁者Meta换成Slave，就得到了半同步复制模式。
具体来说，Master收到消息先本地持久化，然后同时同步给两个Slave，当其中任意一个Slave完成持久化并返回OK后，Master返回OK给客户端。
不难分析，任意一个Slave故障时，都不会影响服务。假如Master故障，则需要两个Slave挑一个出来当新的Master，此时可能只有一个Slave同步到数据，我们需要选择有数据的节点当Master。如果两个Slave都没数据，那任选一个就行。
这里的Slave其实同时承载了“仲裁节点”的角色，当Master和另一个Slave断连时，如果此Slave能连上Master，则支持Master继续提供服务，反之如果此Slave只能连到另一个Slave，那这两个Slave放弃旧Master选个新的出来。
如此这般，这个方案能很好地满足单节点故障时的可用性和一致性，而且规则简单，不需要人工介入就能自动完成。可惜它还是有缺陷的，前面我们其实只分析了单次故障的情形，如果连续多次故障，就不行了。
如图，Master本地写完E=1后故障了，Slave选出新的Master然后写入E=2，随后新Master也故障同时旧Master又活过来了，然后剩下的两个节点都有数据，还都不一样，你瞧瞧我，我瞧瞧你，不知道谁来当Master合适。
你可能想说，我们改下流程，写入时先在Slave持久化，OK返回给Master后再在Master持久化，这样是不是就行了？这样也是不行的，因为Slave可能在刚持久化之后就故障了，随后另外两个节点写入新值并再次故障，最后结果是一样的。
半同步复制还可以进一步打补丁，不过这里我们先放一放，来看一下另一个思路。
多写 如果我们进一步消除节点的特殊性，即不再区分Master和Slave，可以得到另一个方案：客户端把请求同时发向3个节点，当其中2个节点返回OK后，就认为写入成功。
如图所示，Node1和Node2成功持久化了E=1并返回OK，之后Client2再尝试写入E=2时，最多只能写入Node3一个节点，因此无法成功写入，这样我们就保证了手机不可能被卖给2个人。
这里我们利用了“鸽巢原理”：client1和client2要想都写入成功，需要各收到2个OK，而每个节点都只会给第一个请求的客户端发送OK，也就是说总共只能发出去3个OK，因此只有一个客户端能写入成功。
这个规律也可以推广至更多数量的节点，只要规定要求写入的节点数大于一半，就只能写成功一个。
还有一种表述是，两个包含大多数成员的子集，一定至少有一个公共节点。这个性质十分重要，后面我们还会用到。
这个方案的问题在于，它能保证手机不被卖给多个人，但是保证不了手机一定能卖出去。比如3个节点收到的第一个请求分别来自不同的客户端，此时任何一个客户端都无法收集到足够数量的OK。
此外的矛盾之处在于：一方面，节点应该避免先后被多次写入来确保手机不被卖给多人；另一方面，节点又需要能“擦除”已经写入的数据来使得手机最终一定能被卖出。
不难发现，能被安全擦除的值，一定是没有成功写入大多数节点的，一旦写入了大多数节点，客户端就认为写入成功，如果再允许其他客户端写入成功，手机也就被卖给多个人了。
在多写模式下，不存在Master那样的特殊节点，最后手机卖给谁了，不取决于某一个节点，而是由集群中的大多数节点决定。
WRN 多写模式下应该如何去读取数据，DynamoDB和Cassandra所用的WRN模型给出了一个思路。所谓WRN，是指有N个节点的集群，写入时同时写入W个节点，读取时查询R个节点，当保证W+R&amp;gt;N时，同样根据“鸽巢原理”，我们能知道W和R一定至少有一个公共节点，因此先写入的值一定会被后面的读取“看到”。
大家都知道，DynamoDB和Cassandra都是最终一致性的。它们的弱一致性，主要体现在写入进行的过程中进行多次读取，可能有时能读到写入的数据，有时又读不到，根据读取所查询的节点不同而得到不同的结果。
此外，写入成功的值一定会被读到，不意味着读到的值一定写入成功或将要写入成功。假设客户端只写入了一个节点就故障了，数据仍然可能被其他客户端读取到。
WRN还给了我们一点提示，想要集群节点的两个子集有公共节点，不一定要取两个大多数节点，只需要加起一起数量大于N就行了。从高可用的角度来看，W和R分别取刚好超过一半节点通常是一个好选择，因为这样可以容忍最多不超过一半的节点故障。当然了，假如业务只关心写入请求的高可用，完全可以让W=1,R=N，此时只要连上一个节点就能写入，但是不同节点可能写入不同的值，需要在读的时候处理冲突，这就是典型的CAP理论中牺牲C来换取A了。
多读+多写 基于此我们有了改进思路：服务器端总是允许用新值覆盖旧值；客户端使用一种两阶段的流程，在写入之前先进行一轮读取，如果发现已经有值被写入了大多数节点，就说明手机已经被卖出去了，否则可以尝试写入新值。
很显然，与WRN类似，这个方案也有并发问题。当client2发起读取时，client1的写入还没有开始或者进行到一半，此时client2认为没有旧值被成功写入，于是发起写入，而在client2写入成功之前，client1也写入成功了，这样，手机又被卖给了两个人。
这个方案不能成功的原因是，第一阶段的读取的结果不能保持到第二阶段的写入，写入请求到达服务器时，前置条件已经不成立了。
一种可能的改进方法是使用某种锁机制，第一阶段读取时，把读过的节点上锁，第二阶段写入时再解锁。只是这么做的副作用也很显然，一旦上完锁之后客户端崩溃，或者与某些节点的网络断开，某些节点将没有机会被解锁。
我们要做的是把这个锁换成一种“活锁”。
Basic Paxos 在现实生活中有一个活锁的例子，就是拍卖。拍卖的时候，报价是不断上涨的，每当竞拍人给出一个报价时，之前所有更低的报价就失效了，同时产生了一个交易确认窗口期，如果没有人出更高报价，交易就会被确认。
Paxos的工作方式是类似的。每个客户端可以不断生成递增且互不重复的proposal id，写入分为读写两阶段，分别叫_prepare_和_accept_，如果两个阶段之间没有被更大的proposal id打断，写入就能成功。
Paxos把我们之前描述的抢手机的问题抽象为“多个节点共同确认一个值”的问题，把我们的服务器节点叫acceptor，客户端叫proposer，当一个proposer把值写入超过半数的acceptor后，这个值就被确认了。
Paxos的工作过程是，在读取阶段，需要写入数据的proposer向所有acceptor发送自己的proposal id，acceptor保证一旦返回自己的状态，便不再接受proposal id更小的请求了。
我们尝试站在proposer的视角，来推断其收到大多数acceptor回复后，可能遇到的3种情况：
这些节点都没有value，说明此时没有value被确定，而且将来也不会有value被更小的proposal id确定（理由是大多数acceptor已经不再接受proposal id更小的请求了）。此时该proposer可以尝试发送accept消息来写入新值。 这些节点都返回了相同的value和proposal id，说明此时value已经被确定了。此时该proposer应该拒绝掉待写入的新值。 只有部分结果有value，或者这些节点返回的proposal id不完全一样。此时不确定是否有value已经或即将被更小的proposal id所确认，该proposer也不能写入新值。不过，能确定的是，如果已经有value已经或即将被提交，那么该value一定是所有acceptor返回的消息中proposal id最大的那一个（原因参考情况1，某个proposer写入了该value，意味着更小的proposal id都不可能成功）。此时为了得到确定的值，我们只能选择发送accept消息写入旧值。 在第二阶段，proposer把待写入的新值或旧值放在accept消息中发给所有的acceptor，再一次，当收到大多acceptor的返回消息后，该值就被确定了。如果在两个阶段之间插入了proposal id更大的prepare消息，写入将不会成功。这时proposer需要选择更大的proposal id并再次尝试两阶段写入。</description></item><item><title>一个小故事，关于科幻，关于老师</title><link>http://disksing.com/story2001/</link><pubDate>Thu, 10 Sep 2020 00:00:00 +0000</pubDate><guid>http://disksing.com/story2001/</guid><description>今天是教师节，半夜看了罗老师的一个回忆老师的视频，想起些陈年旧事，睡不着了，干脆爬起来写写。
自然，这是个关于老师的故事。
时间回到2001年。这是不平凡的一年，国内外发生了很多大事。911恐怖袭击，中美南海撞机，天安门自焚，北京申奥成功，中国加入世贸，还有一位长者在这一年提出了著名的三个代表……
作为当时一个13岁的少年，我不可能意识到这些事情将对未来世界产生的深刻影响，我最关心的事情是进入初中以后新发展出来的兴趣爱好：科幻小说。
那个时候可以说是中国科幻黄金年代了，《科幻世界》月刊一度是当时世界发行量最大的科幻杂志。很多大家耳熟能详的科幻作家当时都活跃在科幻世界，刘慈欣，王晋康，何夕，韩松，王亚男，不一而足。
印象最深的还是大刘的几部中篇小说。
比如《乡村教师》，这恰好是一个跟老师有关的作品。一个乡村教师倾其所能，竭尽全力把基础物理知识传授给山里的孩子们——虽然这看上去并没什么意义。然而机缘巧合下，孩子被外星人拉去作为“考生”进行了一场考察地球人科技水平的测试，最后成为拯救地球的无名英雄。
比如《朝闻道》。外星人飞船降临地球，他们掌握着地球科学家苦苦追寻的科学秘密。因为不能干涉地球科学的自然发展，任何知晓科学秘密的科学家必须付出生命的代价。后来，全世界最顶级的科学家排着队去从容赴死，只为有生之年一窥科学的真相。朝闻道，夕死可矣。
还有《流浪地球》。小说中的黑暗结局没有被电影拍出来：地球流浪几百年后，科学家预测的太阳爆发并没有发生，愤怒的人们不相信科学证据，把几千名科学家处以极刑。随后，太阳爆发了。
当时，科学和科学家在我幼稚认知里总是既严谨又极其浪漫的，科学知识没懂多少，主要就是向往“科学精神”。很多人小时候对于梦想是什么这个问题，都答想当一个科学家，不知道其他小朋友是什么情况，反正我是当真的，哈哈。
2001年我上初二，新开一门物理课，这实际上也是我所接触的第一门自然科学课程，当然也是特别期待。
物理老师是一个有些微胖的大妈，不说和蔼可亲，说平易近人是不成问题的，平时笑眯眯的，印象中类似食堂打饭阿姨的感觉。
没几天，就迎来了第一节物理实验课。
实验内容很简单，就是两人一组，点酒精灯烧水，把水烧到沸腾为止。事实上，烧水并不是重点，重点考察的是实验方法——整个过程中要用温度计测量水温，并把整个过程的温度变化记录下来作为实验数据上交。
课程在一上来就安排一节这样简单的实验是很有道理的，因为物理学归根到底是一种实验科学，一切理论都是从观测到的现象总结而来。如果理论和实验矛盾了，那一定是理论有问题。历史上，物理学家为了能解释实验结果，就曾多次不惜推翻理论大厦。
水的沸腾没那么复杂。大家都能说出一二：在加热的过程中，水温逐渐升高到100摄氏度，此时水开始沸腾，之后温度不再增高。
然而意外的是，我们当时试了两三次，实验结果都很奇怪，跟课本上写的完全不一样！首先是温度并不如预期那样平缓上升，而是呈现一种震荡波动上升的态势，而且最后根本没达到100度，也就80多度就不再升高了，甚至持续加热之后还波动着往下掉了一小截……
周围同学们陆陆续续做完实验，交了实验报告去吃午餐了。后来同组的小伙伴也有些着急，说我们要不就按课本上的画一个交上去算了吧，旁边组也是没做成功就自己画了。我当然没同意这么干，还把他奚落了一下，虽说我当时也是焦虑，不过脑袋里刷刷闪过的全是伽利略扔铅球，爱迪生测灯丝的故事……
后来别的组差不多都走了，物理老师过来看看我们是什么情况。简单检查了实验器具后，她笑眯眯地安慰我们：实验方法应该没什么问题，出现这个情况，可能只是开着窗，有风吹进来影响了水温。
随后她又补充道：“不过实验数据是错的，不能这样交上去，你们还是按照书上的改正一下吧”。
我不记得当时有没有尝试过抗争一下，只记得最终还是交上去了最标准最优美的水温曲线，只记得走出教室的时候，是大中午，明晃晃的阳光，让人睁不开眼。
说到底，只是一件微不足道的小事吧。
那天中午，如往常一样吃上了午餐。我跟那个同学，一直是很好的朋友。我的物理成绩一直不错，还当了好几年课代表。物理老师一直都是那样的兢兢业，春风化雨。
一切如常。只是就像绝大多数梦想都不会成为现实，我后来成了一名程序员，而不是科学家。
这件小事对我的意义空间是什么呢？很难说清，最大的意义可能是让我提前触及到了某些教育和科研的真相。掐指一算，已经是将近20年前的事情了。时不时地，这件小事就会从脑海深处钻出来。比如听到有知名教授搞学术造假的报道，比如看到如“物理学家新观测结果或推翻现有理论”的新闻……
都说教师是人类灵魂的工程师，此话不假。
其实，每个人都是灵魂的工程师，至少是灵魂的建筑工人。有时候，开一个无伤大雅的玩笑，不经意间的一句话，甚至一个动作，一个眼神，都可能给一个人造成深刻的影响。
讲这个故事并不是想要埋怨或者控诉谁。这个故事里没有恶人，几乎从任何意义上来说，我的物理老师都是一位深受我喜爱的好老师。但是世界很复杂，心灵很敏感，梦想很脆弱。正如罗老师所说的，这个世界，悲剧往往来自善与善的碰撞。
只是在夜深人静的时候，有时我会禁不住好奇，如果当时老师能关上实验室所有门窗，带我们一起再做几遍实验，如果能顺便给我科普下实验控制变量的思想，又或者干脆接收我们真实的实验结果，后来会怎样呢？</description></item><item><title>beancount 复式记账实践</title><link>http://disksing.com/beancount/</link><pubDate>Thu, 20 Aug 2020 00:00:00 +0000</pubDate><guid>http://disksing.com/beancount/</guid><description>尝试复式记账（beancount）有一段时间了，也有了一些实践上的体会（和走弯路），本文就简单做个分享。注意，其实我的很多做法完全算不上专业，甚至十分粗糙，不过我感觉自己简单用用还是可以的，操作起来也比较简单，主要供刚入门的小白参考一下吧。
入门 入门向的中文资料和种草文章有很多了，我这里不准备献丑，扔几个链接算了：
Beancount —— 命令行复式簿记 by wzyboy beancount 起步 by MoreFreeze Beancount复式记账 by BYVoid 复式借贷记账法 by ShanHe Yi 有两个话题大家几乎都有谈到，分别是为什么要用复式记账，以及为什么要用beancount，我也简单谈下感想好了。
在我看来，相对于普通的单式记账（或叫流水账），复式记账的主要优势在于更能体现资金流动的本质。
假如我花15万买了一辆车，开了3年以后卖掉得10万。如果记流水账，从账面上的钱来看，3年前买车花了15万，3年后卖车赚了10万。这似乎符合我们日常生活的认知，但没触及本质。如果使用复式记账，你会得到另外一个版本的故事：3年前钱没有花掉，只是把15万现金换成了固定资产——汽车，而3年后也没有突然获得不菲收入，只是把汽车又变成了现金，这二者差的5万，是在3年不断使用过程中慢慢花掉的。
基于更符合本质的账目，我们将有机会深刻洞察自身财务状况，做出明智的决定。比如上面那个买车卖车的例子，我们会意识到机动车的保值率可能比其绝对价格更能影响其实际产生的花费。另外一个例子是，当我把每个月还的房贷拆解成本金和利息之后，从报表上意外地发现房贷利息占了每月开支的相当一部分，于是促成了我把提前还一部分房贷提上日程。
至于说复式记账软件有很多，为什么要用beancount。我认为beancount主要是对有编程经验的人比较友好，因为是纯文本的，可以很方便地做各种转换，不仅可以自己写一些脚本来自动生成账目，也能把beancount账目导出到别的系统。如果不懂编程的话，就不是很推荐了，图形化界面的软件可能更合适。
工作流 我的做法其实是比较山寨的，很多高级功能都没用。比如我只有人民币一种货币，其他的货币全都转成人民币的入账，再比如我的所有账目都记录在一个单一的beancount文件里（为了方便导出到其他系统）。
我是每个月找一天来集中记账的，平时就完全不考虑记账的事情（感谢无现金时代）。我的情况是每月底至下月初资金变动会比较大，包括发工资、信用卡还款、还房贷，等这些“尘埃落定”后，我就会找比较空闲的一天来记账，记完之后顺便就把资金归置归置，比如不急用的钱扔到余额宝。
第一步是处理微信账单。我的绝大多数交易都是使用微信支付的，包括信用卡也是通过微信来付，主要是因为微信的账单功能特别好用，导出的账单是一张尤为详细的excel表，可以很方便地进行处理。我写了一个简单的脚本，能把excel转换成beancount格式，而且能识别常用的收款方。识别不了的，就需要手动过一遍，标上正确的花费类型。这里常常会遇到想不起来花的钱是怎么花的的情况，可能需要去京东上查订单，或者查当天的聊天记录，或者查当天的日记，一般情况下都是能想起来的。
第二步是所有的银行卡。因为基本上都走微信了，剩下的一般包括工资、房贷、转账，还有少量支付宝的花费和少量的存款利息收入。这里基本就是打开手机网上银行，然后手动录入。这里隆重推荐一下云闪付APP，绑定银行卡之后，一个页面就能显示所有卡的余额了，对账十分方便。
第三步是支付宝。可能会少量付款是用的支付宝付的，需要手动登记一下，还有就是余额宝的利息收入了。
第四步是在公司吃饭的园区卡。这一步是我现在最痛苦的了，消费记录可以在一个APP上查到，但是可惜不能导出，我尝试用fiddler抓包不过也可耻地失败了。现在我的权宜之计是用手机打开消费记录页面，滚动截屏，OCR，再拷贝到电脑上手动调整下格式和识别错误的内容，再用脚本处理一下。
第五步就是各种充值卡了，包括京东E卡，kindle余额，steam余额之类的。这些变动比较少，我处理的比较随意了，我一般有记得的的就打开对应的订单记录一下，记不清就算了，等下次发现对不上的时候再补上。
记法实践 最后分享一下我摸索的一些常见事项的记法吧，仅供参考。
工资收入 工资其实可以记的很细的，比如把五险一金的详细情况都记下来。我对交了多少社保多少个税没太多执念，所以就直接记税后工资了。不过我工资卡、公积卡、医保卡分别是不同的卡，所以我把工资也简单地分成这三块了。
房贷 房贷上面也提过了，记账的时候注意要把交的钱拆成本金和利息，如果直接全还到负债里面，是平不了账的。利息部分我是记成花费（Expenses）的，貌似也有别的记法，不过我觉得记成花费没什么毛病，不深究了。
报销 报销跟前面那个买车卖车的例子有些类似。看似是我花了钱，但是因为这个钱后面公司是给报销的，所以记成花费就不太合适了。我的做法是在资产里用公司的名字建一项应收款账户，花钱的时候，不记花费而是记入应收款，到时候公司给报销了，再把钱从应收款里转出来就行了。同时，检查应收款就能很方便地知道还有多少钱没报销。
信用卡 信用卡一度让我相当凌乱，主要是信用卡本身就有对账日和还款日，然后我每个月的对账日还跟这两个都不一样，在对账日当天很难搞明白我到底应该欠银行多少钱。
后来我重新整理了思路，意识到账目的建模应该跟信用卡本身的内在逻辑是匹配的，信用卡其实同时是存在两个账户的：一个是上月账单一个是本月账单。因此我照葫芦画瓢，给每张信用卡建个应付款子账户。同时，记账当天不对信用卡进行对账了，而是在信用卡的对账日来对账，具体过程是先检查负债是否能对上银行的账单，确认后把所有的钱从信用卡主账户转进应付款，而每当记到还款日时，则清空应付款。</description></item></channel></rss>