普通视图

发现新文章,点击刷新页面。
昨天以前Joway's Blog

昨日的世界不再重来

2026年3月30日 08:00

最近在做一个思想实验,如果你面前有一个按钮,按下就可以让 AI 从世界上消失,你是否会按下。身边很多朋友的答案都是会毫不犹豫的按下,即便有一些人还是那种最拥抱 AI 的人。

拥抱 AI 和厌恶 AI 可以同时在一个人身上发生,我自己就是这样的人。我厌恶 AI,是因为对昨日世界的怀念,我拥抱 AI,是因为今日世界无法回退,我没有不拥抱的选择。

现在的 AI 虽然可以代替我完成很多事情,但是在事情完成后,带给我的并不是成就感而是一种无尽的空虚。有了一个 idea,找一个趁手的模型,用几句提示词让 AI 吭哧吭哧干活,再做几轮调整,最后几个小时项目完成了,剩下一段贤者时间,你不知道这个项目数据库的 schema,也不确定架构是否能支撑多少的用户规模,未来有新的需求,你只能对着这个黑盒许愿,在许愿前完全没有关于需求可行性的任何概念。当 AI 给你完成了修改后,你不知道会不会损坏已有的用户的数据,只能按照 AI 的要求上线。一通操作下来,身体很累,精神很空虚,干了一天的活,又好像什么也没干。伫立在这个黑盒面前,反反复复操作几个月,不知道自己究竟学到了什么。AI 有了更多的数据,一天天变得更聪明,而我们自己呢?

在昨日的世界,虽然我们工作很累,但是工作结束会有一种智力的满足。当其他人来咨询某个需求是否可行,我们可以给出斩钉截铁的答案。当需求可行的时候,我们知道需要修改哪些地方让它可行,当需求不可行的时候,我们知道因为哪些原因所以它不可行。我们面对的是一个充满确定性的世界,我们确定昨天的工作,确定明天的工作,确定自己的职业生涯,确定未来能够有一份收入养家糊口生儿育女。今日的世界一切都不确定,不确定昨天做了什么,不确定明天会做什么,不确定这每天光聊天的工作能否有职业生涯,不确定未来什么样的人才可能凌驾于 AI 之上能有一份糊口的工作。

今日的世界互联网的整体审美也在逐渐倒退。互联网到处充斥着用 AI 生成的机械感十足的图片,也没人在乎是否是真的符合自己脑海中想象的模样,只要和提示词差不多能用即可。各种「高效率人士」用 AI 创作着各种垃圾的文章,宣扬自己最高效的工作流,攀比着各自的 token 消耗量和同时并行启动的 Agent 数量。每天都把一些简单的步骤封装出各种下一个月就会消失的名词概念,并以自己掌握的名词数量而沾沾自喜。

我时常在刷新推特时间线的时候,感受到自己是在一群推土机中漫步,页面滚动,一颗颗大树被连根拔起,群众在欢呼,木匠们做出一件件精美的家具。而我只想回到自己的瓦尔登湖旁边,但是小木屋也早已被掀翻,地上留着一张告示,顺之者昌逆之者亡。

Magic Brush - 画出你自己的产品宇宙

2026年3月5日 08:00

上周末用 AI 写了一个新的产品: Magic Brush

这个产品的 idea 来自于我经常用 AI vibe coding 一些小的前端产品,但是如果每次都为这些小产品创建独立的域名和部署太过于繁琐,而且目前也缺乏一个聚合站点可以浏览其他人创建的小产品。另外,我在写博客的时候,也经常会想要插入一些交互页面来展示我的 idea,但是如果要在博客里插入一堆 js/css 代码又会导致博客无法长期维护。我把这些需求收集了下,整理并抽象出了一个产品形态:

这个产品可以允许用户使用自然语言创作页面,然后通过 iframe 嵌入到任意网页中去。并且这个产品还可以有一个广场功能,可以看到其他人的作品。不仅仅能用来做博客的交互页面插入,也可以作为产品原型设计的工具。

产品设计

Magic Brush 由 2 主要页面组成。

页面设计页

用户可以在设计页右边会话框写自己的提示词与 AI 交互,左边的页面会实时渲染最新的页面。其他用户可以以只读的方式访问该页面,并且也能看到提示词,方便其他人模仿修改并创建自己的页面。

广场页

广场页面允许看到所有 Public 的页面设计,可以以 Like 数量排序或者时间排序:

Use Cases

创作可交互文章

对于有些文章来说,文字并不是最适宜的表达载体,例如游记。你可以把你的游记喂给 AI 生成可交互的页面,然后再插入到文章中。例如我的 «熊野古道中边路纪行» 游记中,可以被转变为:

创作流程图

传统流程图需要用截图的形式插入到文章,导致不易修改。我们可以让 AI 生成流程图页面,然后直接插入到文章中,并且后续实时渲染最新的修改。

创作自定义工具

你还可以根据自己需要,写一个自己用的趁手的前端工具,例如:

甚至能在网页上插入一个小型浏览器:

这些工具都能用 https://brush-api.elsetech.app/pages/cJ3w_m4e3K 的方式作为工具本身全屏打开。

实现过程

Magic Brush 的实现非常简单优雅,整个 Design Agent 在前端运行,后端代码无法拿到用户的 API Token。后端部署在 Cloudflare Worker 上,数据库使用 Cloudflare R2 。整个产品的维护成本接近于 0,除非用户变多。

Design Agent 的设计与传统 Code Agent 类似,约束是产出只能是一个 HTML 文件。在最开始的时候模型会返回一个根据初始提示词生成的 HTML 页面作为起始文件,而在后续 Modify 过程中,只允许调用一系列 tool call 来修改页面,最大化减少了 token 的消耗。

写在 AI Coding 奇点之后

2026年2月26日 08:00

说来惭愧,我已经好几个月没有完全手写代码了,甚至像是把某个变量改一个名字这种活我都宁愿输入更多字符告诉 AI 来做而不是自己直接改了,因为我知道,哪怕是变量重命名这种简单的活,AI 也能做的比我仔细,它会检查所有用到这个变量的代码、相关的注释以及单元测试,然后一并都修改了,而我自己其实历史上就犯过不少忘记修改关联地方的错误。

过去一两年的时间,AI Coding 引来了飞速的发展,就我的个人体验而言,大概经历了三个阶段。AI Coding 1.0 时代,AI 只是简单给你提供代码补全,代码总体还是人在写。而在 AI Coding 2.0 时代,人更像是在指导 AI 写代码,一次 prompt 只能高质量地写一百来行代码,你需要仔细检查代码是否符合你的需求。而最近两个月的时间,AI Coding 3.0 来到了一个奇点时刻,你甚至都不需要精心设计你的 prompt,只要简单描述下需求,AI 自己会根据代码上下文,以极高质量生成成千上万行代码,而人无论是体力还是智力已经无法 review AI 的代码,只能粗浅把握一个编程方向。在两个月之前,我或许还会认为过去的编程工作还能继续苟活一两年的时间,而现在我认为此刻所有传统的编程工作已经在事实上被淘汰,我们这些传统程序员之所以还没有被开除的原因仅仅只是公司自己在制度流程上还没有完全适应 AI 作为员工的方式,而这个适应过程可能会持续几年之久。

在完全不手写代码的这几个月里,我时常在 AI reasoning 的间隙问自己到底在做什么。我似乎做着完全没有技术含量的『传话』工作,但与此同时,如果真要把我淘汰直接让 PM 和 Agent 对话,似乎也很难实现他们的需求,而且出事的风险极高。这意味着我依然在提供自己的价值,只是这个价值无法再被很好的「可视化」。我之所以比 PM 能让 AI 更好地干活是因为我具备一些专有的知识,这些知识的产出可能仅仅只是一个 Yes 或 No,但这不影响他们的价值。我之所以会觉得浑身难受的原因还是之前的日子过得太苦,觉得必须一定要自己手动干很多活才能体现自己的价值,没有真正把自己从一个「劳动者」换位到「管理者」。一个好的管理者肯定不是办公室里最忙碌的那一个人,那些做出对公司最有价值决定的管理者最终的产出也无非就是一个 Yes 或 No。能力强的员工往往希望老板管理的越少越好,AI Agent 也是如此。

我们已经近乎实现了 Coding 的自动化,但是在一个公司里,Coding 只是整个公司工作的很小一部分,甚至都只是程序员工作的一小部分。我们现在拥有了一个无限智能的机器,但是整个公司运行制度都是面向人类设计的,Agent 需要发挥能力必须建立一个正向的 Loop,现在的 Loop 仅仅只建立在 Agent 内部,公司的每个环节与 Agent 并没有建立起这个 Loop,甚至连 Input/Output 渠道都没有建立。未来 Agent 会进一步渗透进整个公司一切流程的方方面面,不仅改变软件工程师的工作内容,也会改变产品经理,HR等一切职能的工作内容。我觉得这些传统工作岗位的消失会成为必然事件,但并不意味着不会创造新的工作岗位。最可能发生的事情是工作岗位从过去的不断细化的趋势开始变得不断泛化。现在你还在那区分前后端工程师属实是一个笑话,各种软件工种现在都可以统称为工程师岗位,如果一个后端工程师现在还写不好前端需求只能说明你不是一个合格的工程师。而所有工程师的工作会变成编排管理成百上千 Agent 的编码任务,解决 Agent 运行过程中遇到的无法自我解决的问题,帮助公司完善流程让 Agent 干活干得更舒心。

尼泊尔布恩山小环线纪行

2026年1月11日 08:00

2025 年 12 月跨年之际,为了赶在假期作废之前销假,我报了国内稻草人的一个徒步团,前往尼泊尔安纳普尔那山区进行了为期四天的 Poon Hill 小环线徒步。

尼泊尔最有名的两条徒步线路分别为 EBC 和 ABC。就 EBC 而言,我目前的身体状态和高海拔徒步经验远不足以应对,ABC 相对简单一些,但是依然有高反的风险。由于我印象里自己似乎从未到达过海拔超过 3500 的地区,所以我对自己身体适应高原反应的能力完全没有认知,于是就退而求其次报名了最高登顶海拔 3800 米,最高过夜海拔 3200 米的 Poon Hill 小环线,以此作为我高海拔徒步的入门路线。

Day 1

第一天的路程主要以爬升为主。从 Ghandruk 到 Tadapani,累计爬升 1116 米,下降 112 米,住宿海拔 2718 米。

第一天的景色比较一般,全程主要在林子间穿越,最后还走了一小时不到的夜路才抵达住宿点。

Day 2

第二天路程较短,但是徒步海拔较高。从 Tadapani 上升到 Dobato,累计爬升 848 米,下降 108 米,住宿海拔 3456 米。

早上起来,我们就在 Tadapani 看到了壮丽的安纳普尔纳南峰和鱼尾峰的日照金山:

这一天的路上我们还时不时能从林子缝隙里从不同角度看到这两座山峰的容貌,在路程末尾还看到了道拉吉里峰和Tukuche peak。

这一天我来到了有生以来最高的过夜海拔,在路上的时候倒还是适应,但是到了住宿的地方休息了一阵子之后,发现一做剧烈动作就有点晕,问了下 ChatGPT 似乎是这个海拔的正常轻微高反表现。这天气温也特别的冷,住宿条件也不高,甚至房间还有点漏风。

2025 投资组合年报

2026年1月8日 08:00

2025 年,股市经历了政治的风云变化,也经历了 AI 浪潮的大洗礼。我的整体投资组合也引来了巨大的变化。这一年,标普 500 的年回报是 16.35%,而我的投资组合的回报只有 4.0%,远远跑输了标普 500。

其实在 2024 年底 的时候,我还有 86.4% 的持仓还是在 VOO 和 QQQ 两大ETF,那时候 AI 浪潮才刚起步,世界政治局势也波澜不惊,在我不需要大量现金支出的情况下,这样的持仓非常舒服。

然而川普上台后,在 4 月疯狂玩弄世界经济,而美国的政体却对这样的疯子没有一点制裁的办法,甚至还有一堆拥趸对其齐呼万岁,这彻底动摇了我对美国体制的信心。所以在四月大跌到一半的时候,我大幅度卖出了美股的持仓,在下跌到接近最低点的时候,又卖出了一批,卖出的资金后续陆续投向了美债市场和新加坡股市。当时伴随着美国股市,新加坡股市也引来了大跌,许多优质股如 DBS 引来了历史最佳位置,所以我乘机入手了一些 DBS 和海峡指数基金。

然而后半年让我意想不到的是,标普 500 依然表现强劲,即便川普继续做了更多疯狂的操作,美股似乎依旧不为所动,持续创下历史高位。所以从后视镜看,我的调整无疑是失败的,但我的目标始终是建立一个稳健可持续并舒服的资产组合,从这个角度来说,我目前的组合是真正能符合我的目标的。我现在的持仓基本维持着股债分别占 50% 的水平。其中股票部分里,26% 在新加坡股市。

在绝大部分情况下,这样的组合基本可以让我一整年都不需要特别关心股市,实际上我可能现在打开证券软件的频率是一个月一两次。在美股大涨的时候,我的收益也不会差的特别多,美股大跌的时候,我甚至会特别开心,因为终于可以操作调仓补进美股了。

但是我有一件事情其实到如今也想不明白的是,这一次的调仓,究竟是我对美国信心的动摇,还是我因为下跌而恐慌了。如果是因为后者,我就无法原谅自己了,因为在美股下跌的同时,我还有另一个稳健账户里存有不少的新加坡国债,我完全可以动用那笔国债进行抄底,这对我的持仓组合来说是巨大的天赐良机。但如果我是因为真的对美国的信心动摇,那么我的操作就是正确的至少是不应该后悔的。即便我十分擅长反思自己,但我至今也无法分辨自己当时的心态属于哪一种。我也深刻认识到「知行合一」的困难,它远远不是你知道了某件事然后就去做某件事这么简单,而是反过来,你做了某件事,你却不知道自己是因为哪一种「知」而做了这件事。而人性往往会倾向于帮你选择那一个你最希望的「知」来合理化你的行为,最终真正的「知」就消失在了一堆道不清的妄念之中了。你脑子里所臆想的你自己也会远远和你实际行动塑造的真实自己所背离。

哎,学吧,太深了!

三十而笠

2025年12月21日 08:00

到 12 月底,我就迈过了人生的第三十个年头,进入到奔四的阶段。听年长的人谈起过,人对时间流逝速度的感知是指数变快的。可能这也是为什么对我而言,抵达三十岁的路程是极为漫长和艰辛的。

这三十年里有近二十年的时间,我的人生是不受自己把控的。你无法决定自己应该看什么书,学习什么知识,交怎么样的朋友,尊敬怎么样的人,甚至连每天吃什么都无法决定。上帝给你随机分配了一个国籍,一个家庭,你的国家不管你认同与否强制把你塞进了一个社会制度,你的家庭又直接决定了你接近二十年的生活质量和初始品格。我的前二十年基本就是活在被权威压迫的氛围中,在我对世界的理解尚浅时,我被中国老师以严厉为美德,以恐惧为工具的教学方法所压迫,而最糟糕的是,我一开始甚至都没有认识到这是一种压迫,你可能还会真的相信老师是在“真心为你好”。“真心”大部分时候倒是没错,没有老师会主观希望学生差,但是客观上,整个中国的教育制度出发点更多是“为了国家好,为了集体好”,而非真心为了你个人好。如果是为了你个人好,那无论你贫穷还是富有,聪明还是弱智,你都应该从这个教育制度里获益,成为了一个更好的自己,但是显然这个教育制度一切的出发点是为了选拔能够更好地建设国家的人,而不在乎每一个个体最终的结果如何。

我花了快三十年时间才想明白一件事情,最好的考试成绩其实应该是零分。学习知识的目的并不是为了考试,而是为了学习知识,考试的目的是为了检查哪一部分知识没有学到,如果你考了一百分,意味着你白花了几个小时做检测,没有找到半点没学到的知识。而只有以选拔为目的的考试,分数才是越高越好,但即便你拿到了满分,这也只能证明你掌握了这极小一部分的知识集合,且你超过了人类中极小一部分人群,而整个教育体系只覆盖了极少一部分知识而且有些知识还是被歪曲的,在这样的高度偏差还不干净的数据集下花上十多年的青春来训练得出一个高分,我实在想不到有什么更蠢的行为了。

这个道理虽然如此浅显易懂,却在这个社会里属于歪理邪说,我也断然不敢和任何二十岁以下的人灌输这种观点,因为我知道,在这种教育体制下,你要逃脱这个愚蠢的游戏,唯一的办法就是首先尽量在这个游戏里取得高分,然后快点熬过青少年时期,成长到一个你有自主决定权玩什么样的游戏的年纪。如果你不慎在青少年时期就明白这个道理,却又不得已要继续玩这个游戏,除了让你更加痛苦没有任何好处。庆幸我小时候网上没有人写这种大逆不道的文章。

在好不容易熬过前二十年后,我的人生终于引来了可以自主决定近乎一切事情的阶段。我在前二十年的那场游戏里并没有取得高分,只上了一所普通一本大学,但是所幸的是我成功读到了我唯一想读的计算机专业。我大学时候唯一的客观制约就是最好不要被学校开除以及尽可能拿到毕业证书,在这个底线之上,我几乎可以做任何合法的事情。我完全可以无视常规的从大一到大四的学习路径,在大一学习大四的知识,在大二就开始实习积累工作经验,在大四别人实习的时候我去世界各地旅行,而且并没有任何人会因为我的目无章法而指责我相反倒是收获了不少羡慕和赞美。恰好这段时光也是中国互联网乃至全行业发展最好的时间段,对我来说,我根本无需担心现在做什么和没做什么会影响到未来,因为那时对未来预期已经美好到现在做任何事情都影响不了的程度。

在二十岁后的前五年里,我高频跳槽,并不断尝试做新的事情,学会了后端就开始学前端,学会了安卓开始学 iOS,做完了业务开发做基础设施,以工作时间来讲,那个时候是我工作时间最长的时候,甚至周末也会真的自愿加班来学习新的知识,但同时这段时间也是我工作最开心的时候。在后五年里,随着我逐渐打工越打越深入,以及打工的公司越来越大之后,工作的重心慢慢从“做有意思的事”转变为了“做有利于晋升和绩效的事”。我刚毕业那一会儿,虽然绩效也很不错,但其实根本是不管绩效在干活的,一门心思只想做有意思的事情并把事情做好。而且说实话我根本不在乎什么绩效和老板的看法,因为我已经拿到了学位证了,消除了这个社会制约我的最后一个把柄,从此我做任何事情,只要它合法,那最差的结果无非是公司把我开除了,而这个最差的结果根本对我就无从轻重,况且谁开除谁还不一定呢。然而这个淳朴的少年慢慢地在社会的锤炼下被一点点磨平了心气,我半主动半被动地,被带入了“成人高考”的游戏 —— 优绩主义。

在我这些年的打工生涯里,我也逐渐认识到人和人之间有着巨大的分别,每个人工作的目的也多种多样。有赚钱养家型的,有生活工作平衡型的,有埋头只想晋升型的,还有像我一样只想纯粹写代码型的。可能就是因为这种差异性,公司必须发明一种统一的价值衡量体系,不管每个人主观意愿如何,只要你还想在这个公司干,甭管你个人喜欢什么型,都必须围绕着这个公司制定的价值衡量体系转。是不是很熟悉?这和中国的教育体制如出一辙,甚至还更残酷和荒谬。考试的试卷对每个人都一样,评判成绩的标准也是统一和公平的,而且每个人都可以考一百分。但是企业的绩效制度却充满了人情世故甚至是尔虞我诈,并不单纯只是你对公司有贡献就能取得好的绩效,而且你绩效好大概率就有人得要绩效差。有时候我会觉得大家很可笑,就多和少那点奖金,何苦一整年全部一门心思在这个上面勾心斗角。工作的目的原本应该是把工作本身给做好,现在反倒成了一定要比别人做的“好”,且得是你老板所认为的“好”。我不喜欢这个游戏并不是我无法赢这游戏,恰恰相反,我大部分工作年份拿的都是好的甚至是 Top 的绩效,我和我大部分老板也都是非常好的朋友,只是最近这些年互联网公司以及其中的员工有点在这个游戏里玩的太入戏了,让我非常出戏。

所幸的是,经过这些年的积累,我多少也具备了能够逃离这种游戏的能力和底气。在 2025 年初,我给自己立下了一个目标,在 30 岁之前,尽可能不做自己不喜欢做的事情,达到自己想达到的生活状态。为此,我换了更健康的工作,恶补了英语,锻炼了身体并近乎解决了长期困扰我的颈椎病,还申请了新加坡公民。我不敢说自己达到了这个目标,但至少比年初的时候要距离目标接近了一大截。

最重要的是,我认为我现在在玩一个我真正喜欢玩的游戏。我可以自由地学习我想要学习的知识,我也可以自由地选择不学习只摆烂,我可以选择去赚更多的钱,我也可以选择少赚一些钱多出来一些时间锻炼身体,我的社会成就比很多人低的多但是没有人会以此来评价我个人。我其实明白,这个游戏的危险之处在于,这是一个高度以自我为中心的游戏,脱离了任何客观评价的制约,最终容易度过一个碌碌无为的一生。但是,又有谁能如此武断地去评价一个人碌碌无为呢,每一朵花开自有它的原因,不一定要被你看到才是实现了一朵花的价值。

我小学就很喜欢《江雪》这首诗和它的配图,甚至在课桌板下画了“孤舟蓑笠翁”的画,没想到二十年过去,我终于活成了“南洋蓑笠翁”。

我的颈椎病康复之旅 - 关于选择的故事

2025年6月29日 08:00

从 2014 年上大一开始到现在,我的颈椎已经日均面对电脑超过 8 小时 11 年了,世间的物理器件应该很少有能工作超过 10 年之久的寿命,而我的颈椎也在差不多工作到第 10 年的时候出现了严重的问题。

其实早在四年前的时候,我的颈椎已经给了我警告。但那时候只是偶尔会出现僵硬疲劳的感受,通过每周的按摩差不多都能有所好转,如果专心于工作,其实并不能时刻感觉到不适。后来拍片发现了颈椎已经开始出现了生理性变直的问题,但是由于现代人有相当一部分都有相似的问题,所以我也并没有很在意。并且依然维持着每天十点多到晚上八九点钟的高强度工作节奏,更为糟糕的是,我在那时候还长期保持着趴睡的习惯,让我的颈椎曲度一天 24 小时都没有一个正常的状态。

这种生活方式维持了差不多两年多的时候,我的颈椎问题已经恶化到了两眼偶尔会模糊,严重时已经无法正常工作的程度了。并且此时按摩已经连短期恢复的效果都达不到了。最严重的一次是,我因为脖子难受而在旋转脖子的时候,似乎不小心压到了一根神经,导致那天我甚至都紧急去了急诊室。那次遭遇让我意识到这个问题已经严重到没有任何其他问题能超过的地步,我甚至都为此备好了后事比如把我的银行账号都发给了我姐一防哪天真出现了意外。从那天之后,我开始下定决心要认认真真解决这个问题。

我在新加坡先是去拍了 MRI ,以确认我颈椎是否真的有物理上的形变问题,但是结果是除了正常的曲度变化外,并没有看到有其他严重的问题。此后,我又去看了专门做美式整脊的 Discover Chiropractic 诊所的 Dr Joachim Low,通过他的正脊治疗,反倒是彻底解决了我之前伴随着的腰痛问题,迄今为止一直没有复发,但是我的脖子问题却丝毫没有改善甚至还有加重。接着我又去看了专门的脊柱专科医生 Dr Wu Pang Hung,他也根据 MRI 认为我的脊柱本身没有什么问题,给我开了加强骨关节的药物,但是也没有什么效果。后来我又专门找了一家名叫 The Pain Clinic 的 Dr. Ho Kok Yuen,他给我做了验血发现我有维生素 D 缺乏的问题,先是给我开了一些维生素片,然后在脖子上针对疼痛的部位注射了类固醇,在注射完的当下,确实感觉效果非常不错,但是一般维持个三四天疼痛感就又会回来,并且通过一个多月的高剂量维生素 D 补充也没有发现有丝毫改善的迹象。在实在不行的时候,我就又会回去找 Dr. Ho 再给我来一针。我觉得这也不是长久之计,最后,我去看了 Singapore Paincare Center 的 Dr Bernard Lee Mun Kam ,这是我整段治疗过程的最后一站,而 Dr Lee 也是世界上我除了父母之外最感激的人,甚至可能都没有之一。

在 Dr Lee 的诊断下,他认为我的根本性问题还是在于神经而非物理的颈椎或是肌肉上。但是在神经的影响下,我的颈部肌肉已经出现了严重的筋膜炎,所以需要先用一种叫做 PRP(Platelet-Rich Plasma) 的血小板注射手术,快速让脖子肌肉先恢复,然后再使用抗抑郁症的药物去舒缓神经的紧张问题。我是在 2024 年 8 月 31 日做的 PRP 注射,一共花了 6500 SGD ,注射打了局部麻药,所以那天我回家又睡了一整天。但是注射完后,我感觉真正意义上的生活又重新回来了。与此同时,我还报了 20 节私教课,每周训练肌肉力量,让身体找回年轻的感觉。在 10 月份我的身体状态已经恢复到了能完成环勃朗峰高强度徒步的程度。

熊野古道中边路纪行

2025年5月12日 08:00

大约是在六七年前第一次听说熊野古道,后来又在任宁的播客 声音:时速三公里 里,对这条路线有了更加清晰的认识,暗暗把它加入了愿望清单。后来由于疫情的原因迟迟未能如愿,终于在今年 5 月,得以来到纪伊半岛踏上了这条古老的朝圣之路。

纪伊半岛毗邻京都,奈良和大阪,在日本历史上一直是重要的佛教圣地,众多高僧名士在此活动,他们往来半岛各处庙宇间的道路便逐渐成了现今的熊野古道。熊野古道分为多条路线,有大边路,小边路,中边路,伊势路等。我们此行选择的是住宿相对最容易预定,难度也适中的中边路。

当地的观光协会在途径的神社门口会设立印有神社图案的盖章点,朝圣者集齐特定数量和路线的盖章,可以前往特定几个城市的游客中心获得特别的踏破认证。但由于其中一些匪夷所思的流程设计 —— 比如盖章本是在徒步起点十多公里处才会售卖 —— 就我的体验来看,盖章活动和纯粹徒步之间衔接的并不是很好,如果你追求的是集齐所有盖章,那么你不得不走回头路,或者单纯为了盖某个章去坐公车。出于不想舍本逐末的原因,我们放弃了一些并不顺路的盖章点,只走了全程的大概 90% 顺路路线。

全程行迹图:

朝圣盖章:

行前准备

熊野古道成行最大的困难点在于中途的住宿预订,其中绝大部分停留村镇基本无正规意义的酒店可言,当地民宿也大多没有入驻到传统预订平台,而是要通过熊野古道官方的预订网站 kumano-travel 进行预订。这个网站每点击都要等待约 10 秒钟,而且你需要一次性把你的徒步行程都规划好,然后一次性预订所有沿途住宿,住宿提供方不仅会审查是否有空余房间还会检查你的行程规划合理性,如果有其中一个点无法成功订到住宿,整个预订单都会失败。由于 5 月初恰好赶上了日本的黄金周,所以我提前了 6 个月就开始预订,但即便如此也有很多住宿点已经没有空余 slot 了。

纪伊半岛 5 月大概处于春季,加之一共也才 4 天的徒步,为了减轻每天徒步的背负重量,我把随行装备减少到了极限,只有身上穿的一套春装和备的一套春装,以及一些急救装备,电子设备和登山杖。总共仅 5 千克。

Day 1: 滝尻王子 到 近露王子

上午 9 点坐公交从纪伊田边抵达熊野古道馆,正式开始徒步。这天上午一直下着小雨,不过所幸这是这四天旅程唯一一段雨中行走。

一个半小时后,抵达一处展望台,依稀可见远处群山。

中午 12 点,抵达高原的休憩所,天气彻底转晴,在阳光下解决午餐。

下午全程都在森林中行走,雨后山蟹也跑到了道路上,空气异常清新。

下午 4 点 30,抵达近露。远看近露只是一个小村庄,但却在这里找到了相比后面几天最大的超市。

第一天一共行走了 16.8 km,爬升 1000 m。

Day 2: 継桜王子 到 熊野本宫大社

上午 8 点坐巴士抵达継桜王子的山脚处,开始今日的徒步。今天大约有 1/4 的路程都在马路上,但是道旁的景色一点也不输山里。

重新思考 Go:了解程序在线上是如何运行的

2024年12月10日 08:00

重新思考 Go 系列:这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题,探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。

前言

过去一段时间,在大量的线上服务 case study 过程中,逐步深入了解了如今的业务 Go 进程是如何在一系列繁杂的基础设施之上运行的。有些表现在意料之中,也有一些出乎意料的发现。

我很喜欢一个叫做「 机械同理心/Mechanical Sympathy 」的概念,大体的意思是你必须深刻了解你的程序/机械装置是在一种怎么样的环境下运行的,设身处地地在这个运行环境下去思考,才能帮助你写出更好的程序,或是解答一些奇怪现象的原因。

本文希望达到的目的,也是构建这份「机械同理心」。

程序能使用多少计算资源?

对于很多人来说,这个问题似乎非常简单,大多的计算平台都会要求你建立服务的时候就指定好「需要的计算资源」,但这与你的「能使用的计算资源」是两件事情。实际上这个问题的复杂性远远超出大部分人现象,甚至都不是一个常量,也无法使用公式简单计算。抽象地讲,这取决于天时地利人和。

  • 「天时」指的是内核 Cgroups 以及容器调度平台的基本原理和参数设置。这是硬指标,大部分时候也是常量。
  • 「地利」指的是部署的实际物理机的繁忙程度。
  • 「人和」指的是写程序的人得在能够有更多计算资源可用的时候真的让自己程序用上这些计算资源。

接下来我们逐步分节来详细探究这个问题。

被封装的 CPU 谎言

在 Oncall 中常见的一个误解是,研发人员在容器平台上申请了 4 核 CPU 的容器,然后自然而然认为自己的程序最多只能使用 4 个 CPU,于是按照这个计算能力去估算需要的容器数量,以及对自己程序套上这个假设进行参数调优。

上线后,进到容器用 top 一看,各项指标确实是按照 4 核的标准在进行。甚至用 cat /proc/cpuinfo 一看,不多不少刚好看到有 4 个 CPU。。。

但实际上,这一切都只是容器平台为你封装出来的一个美好的假象。之所以要把这个假象做的这么逼真,只是为了让你摆脱编程时的心智负担,顺便再让那些传统的 Linux 观测工具在容器环境中也能正常运行。

但是假象终究不是真相,如果有一天你遇到一些疑难问题或是想要去编写极致性能的代码,你会发现这些封装出来的抽象把你的理解带的有多偏。

我到底能同时使用多少个 CPU ?

在 K8s 的环境里,CPU 是一个时间单位而非数量。正常情况下,一个拥有 64 核心的机器,你最多能同时使用的理论 CPU 个数是 64 ,即便你创建容器的时候申请的是 4 核。

欧游散记 —— 特摩索斯古城

2024年4月7日 08:00

特摩索斯(Termessos)位于土耳其南部城市安塔利亚的北面大约 30 公里,是一座起源神秘,消亡也神秘的深山古城。它第一次被历史提到是在公元前 333 年亚历山大大帝包围这座城市时,但即便是征服了希腊世界的亚历山大大帝也并未能成功征服特摩索斯。

特摩索斯建于海拔 1000 多米的山口位置,由于特殊的地理位置,加上完善的水利设施,所以即便被包围也只需要一小只军队便可守卫,这帮助这座城市在那个群雄逐鹿的希腊化时期和罗马帝国时期得以一直以独立城邦的状态存活下来。一直到公元 5 世纪的一场地震摧毁了这座城市的蓄水库,导致居民不得已陆续搬迁,最终荒废并被人们所遗忘。一直到 19 世纪,欧洲的探险家才重新发现了这座城市,但即便到今天,特摩索斯一直是一个非常冷门的旅行目的地。

我知道特摩索斯,是机缘巧合在 YouTube 搜索安塔利亚的视频时,偶然间看到了一个特摩索斯徒步视频,即便画质极其粗糙,依然还是被这座山顶古城给震撼到了。更因此将安塔利亚作为了土耳其旅行的其中一站。

到达安塔利亚,我们在 Airbnb 上约了一个本地向导 Onder 带我们开车到达北部的居呂克山-特摩索斯国家公园,从国家公园徒步前往特摩索斯所在的山顶。Onder 的本职工作是老师,但十分热爱特摩索斯甚至他的硕士论文写的课题便是特摩索斯,所以业余时间也通过做特摩索斯的向导赚点外快。

虽然特摩索斯的城市部分主要集中在半山腰和山顶,但是山底相当于这座城市的郊区,和现代城市的郊区一样,往往是平民的生活聚居地。

首先看到的是城市的宗教区域,哈德良庙,但如今只剩下了一扇门孤零零地在山脚下伫立,既神圣又凄凉:

在哈德良庙的另一边,是居民的墓地区。特摩索斯的墓十分有特色,都是以露天石棺的形式“展览”在地表之上。石棺的雕刻也别有一番讲究。

普通平民的石棺是以两个太阳加中间方块的形式,太阳是特摩索斯的标志,这里也被称之为太阳城:

而士兵的石棺会在太阳上增加武器的标识,寓意太阳的捍卫者:

无论是平民还是士兵的石棺都略显单调和同质,特别是人死后,棺材还会被摆在地表被长久展示,这导致石棺本身变成了一个人生命价值的化身。如果没有钱请不起好的雕刻工匠,就靠战斗获得荣誉来装点自己的石棺。如果有钱,就极尽工匠精湛的工艺让自己的石棺变成华丽的艺术品:

又或者是更为有权势的领袖,可以直接在岩壁开凿更为壮阔的仰望式墓藏:

考虑到两千年后,我们还依然在为他们的石棺赞叹,当初他们的「虚荣」如今也被岁月洗涤成了「实荣」。

沿着山路,陆陆续续会走过众多城墙和古罗马式引水渠,以及如今已不知为何物的废墟:

在半山腰,还能看到一个保存地非常好的体育馆遗址:

以及曾经的祭坛:

接着便来到了特摩索斯城市最最核心的基础设施 —— 蓄水库:

由于地震已经毁坏了蓄水库很大一部分,所以今天的蓄水库为了维持稳定,能够看到有很多人工固定的痕迹,但是能够两千年前在一个山顶开凿一个如此巨大的地下空间还是非常令人震撼的。

大约花了一小时的路程,便可抵达特摩索斯的山顶,在一个转身间,看到了我在土耳其见过的最震撼的一幕:

特摩索斯在人类历史上,只是一个非常短暂,也没有名气,更没有对任何历史节点产生重要影响的小城市。特摩索斯的统治者也并非历史上赫赫有名的王侯将相,甚至这座城市更像是纯粹的自治军事城邦而非君主制的王国。但是如此小的城市居然会为了其居民建立如此恢弘的一个剧场。更何况这里不靠近雅典也不靠近罗马,地处希腊世界的边陲。

站在特摩索斯的山顶上,我眼望着希腊文明的「边际产物」,如同看到了一个外星文明一般陌生。从山脚下看到的自我价值实现和山顶上对自然与人类文明极致的审美表达。特摩索斯的居民在群雄逐鹿中维持了独立,依靠工程学知识建立起了少见的防卫居住一体的水利设施,甚至还在这种山城里建立起了不输其他同等规模但地处丰饶平地的希腊城邦的剧院。

我在之后的土耳其境内的希腊化古城邦旅途中,也遇到了更多比特摩索斯大得多,恢弘得多的城邦,但是特摩索斯一直是最特别的那个,也是我记忆最深的一个。以特摩索斯作为我在古希腊-罗马世界的游历起点,很难想到比这更好的开始了。

重新思考 Go:Channel 不是「消息队列」

2024年3月31日 08:00

重新思考 Go 系列:这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题,探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。


Go 语言是一门为实现 CSP 并发模型而设计的语言,这也是它区别于其他语言最大的特色。而为了实现这一点,Go 在语法上就内置了 chan 的数据结构来作为不同协程间通信的载体。

Go 的 channel 提供 input 和 output 两种操作语法。input 一个已经 full 的 channel ,或是 output 一个 empty 的 channel 都会引发整个协程的阻塞。这个协程阻塞性能代价很低,也是协程让渡执行权的主要方法。

ch := make(chan int, 1024)
// input
ch <- 1
// output
<-ch

然而 channel 的实现恰好和进程内消息队列的大部分需求是吻合的,所以这个结构时常被用来作为生产者消费者模型的实现,甚至还作为 channel 的主流应用场景而推广。

但事实上,如果真的把该数据结构用来作为系统内核心链路的生产消费者模型底层实现,一不留神就会遇到雪崩级别的问题,且这些问题都不是简单的代码修改便能解决的。

Input 失败导致阻塞

当 channel 满的时候,<- 操作会导致整个 goroutine 阻塞。显然这并不总是编程者希望的,所以 Go 提供了 select case 的方法来判断 <- 是否成功:

select {
case ch <- 1:
default:
 // input failed
}

但问题是,当 channel input 失败时,编程者还能怎么做?除非队列的消息是可以被丢弃的,否则我们可能只再去创建一个类似 queue 的结构,将这部分消息缓存下来。但是这个 queue 的结构可能又要和这个 ch 本身的队列顺序处理好并发关系。

重新思考 Go:Slice 只是「操作视图」

2024年3月30日 08:00

重新思考 Go 系列:这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题,探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。


Go 在语法级别上提供了 Slice 类型作为对底层内存的一个「操作视图」:

var sh []any
// ==> internal struct of []any
type SliceHeader struct {
 Data uintptr
 Len int
 Cap int
}

编程者可以使用一些近似 Python 的语法来表达对底层内存边界的控制:

var buf = make([]byte, 1000)
tmp := buf[:100] // {Len=100, Cap=1000}
tmp = buf[100:] // {Len=900, Cap=900}
tmp = buf[100:200:200] // {Len=100, Cap=100}

虽然 Slice 的语法看似简单,但编程者需要时刻记住一点就是 Slice 只是一个对底层内存的「操作视图」,而非底层「内存表示」,Slice 的各种语法本身并不改变底层内存。绝大部分 Slice 有关的编程陷阱根源就在于两者的差异。

Slice 陷阱:持有内存被「放大」

以最简单的从连接中读取一段数据为例,由于我们事先并不知道将会读取到多少数据,所以会预先创建 1024 字节的 buffer ,然而如果此时我们只读取到了 n bytes, n 远小于 1024,并返回了一个 len=n 的 slice,此时这个 slice 的真实内存大小依然是 1024。

func Read(conn net.Conn) []byte {
 buf := make([]byte, 1024)
 n, _ := conn.Read(buf)
 return buf[:n]
}

即便上一步我们内存放大的问题并不严重,比如我们的 n 恰好就是 1024。但我们依然会需要对连接读到的数据做一些简单的处理,例如我们现在需要通过 Go 的 regexp 库查询一段 email 的数据:

那一天,我决定踏出一步

2022年5月10日 08:00

四月与五月之交,我完成了人生中迄今为止最惊心动魄的一次冒险。与通常的冒险经历不同的是,一般的冒险人们总愿意在往后的岁月里反复回忆甚至加工,但我的这次冒险我希望此生永远的忘记,但我又明白,之所以我如此迫切地想要忘记,是因为这段经历将会不可逆地影响我一生。

过去的那个少年已经被这场冒险所杀死,我只是作为一个旁观者,在叙述一个已经去世了的人在那几天的经历。

这场冒险的源头还要从 3 月开始说起。

3 月初,我在北京出差,在返回上海的前两天,当时因为北京有人在乌克兰大使馆门口献花,导致三里屯的使馆区莫名其妙被政府封了。我和朋友那天晚上临时起意,打算去现场看看,于是就绕着乌克兰大使馆走出了一个方形的圈。这件事情与后面发生的事情并没有什么直接的联系,但它确是我在中国境内拥有的最后一次自由行走的经历,而我在三里屯画下的这个圈,也成为了后面发生的事情的一个隐喻。

2022 年 3 月 4 日,我从北京回到上海,而这一天北京办公室却检测出了一例阳性,导致我回到上海后被判定为所谓的「高危筛查人员」,于是就开始了原定于 14 天的居家隔离。然而从 3 月 16 号开始,整个打浦桥街道开始出现了非常多核酸异常的情况,我小区在内的相当一部分小区变成了所谓的封闭管理。每天都需要做一次核酸,但是小区里的居民此时并不知道问题的严重性,甚至我怀疑连居委会都不见得知道真实的核酸异常数据,所以在做核酸时,大家有说有笑,许多老头老太甚至在排队时都不戴口罩。

当时所有人的预期是,所谓的 2+12 封闭管理就是只需要小区被封闭两天而已。这对于年轻人来说,可能平常出小区的频率也就两天一次,所以几乎没有任何影响。并且此时我们依然可以在小区内自由活动,去门口取外卖。

3 月 18 号,这是原定小区解除封闭管理的一天,而我个人也早已满足了 14 天的居家隔离要求。那天中午,我兴冲冲地跑下楼,想要去商场里面吃一顿好的,结果发现小区并没有如期的解封,反而是又贴了一份继续 +2 的通知。此时我的愤怒在于它打破了我们原先的预期,导致原先的欢喜落空,但我依然相信只要再坚持两天就能解封。

3 月 20 号的时候,我楼道门突然在毫无事先通知的情况下被一道铁链锁住。试图去推开一道推不开的铁门,这种屈辱感和无力感永生难忘。

20 日晚上等了 2 个小时,才吃到已经在外面冻冰了的鳗鱼饭,21 日中午又等了 2 个小时拿不到外卖然后吃的泡面的时候,而当天晚上彻底外卖就没送过来了。那时候才第一次知道原来饿久了,眼睛会花手会抖。不知道明天会发生什么,但当天晚上我就把恒生指数的基金清仓了。

23 日已经彻底放弃了外卖这件事情,中午八宝粥,晚上吃泡面加饼干。接下来的每一天都接近于此。点外卖几乎只能靠金钱加上运气,偶尔会有一些商贩“违规”在外卖平台上上线一些熟食之类的东西,那时候偶尔才能够有一点加餐。甚至有一次还抢到了能够让我吃上两天的两桶麦当劳全家桶。

再接着就是大家所熟知的 4 月开始上海进入事实上的全面封城状态。

我所在的黄浦区,在整个上海属于重灾区,而我的小区又是黄浦区的重灾区,我所在的楼栋又属于小区里的重灾区,先于上海绝大部分小区率先封闭管理。上海发布每过几天通报一次我楼栋有了新的阳性。截止我离开的时候,楼栋里阳性户数至少也有 30%,悲观估计可能多达 50%。

4 月 18 号的时候,我明白自己不可能继续这种生活,开始规划逃离上海的行程,于是给居委打了电话咨询了开出门证的要求,让我意外的是,虽然我的楼栋形势严峻,但是给我的要求却还算合理(当然事实证明我天真了):

  • 楼栋 7 天无阳性
  • 48 小时核酸阴性
  • 全程闭环车运输
  • 前序航班到达

其中其他几项都可以通过花钱解决,唯独所在楼栋七天无阳性这一项对我来说充满了不确定性。对于在上海的绝大部分人来说,这个要求确实并不过分,也很容易达到。但对于我这栋重灾楼来说,就需要时刻算着一个 7 天的窗口期,到了窗口期立马走。

True Story

2022年4月28日 08:00

2015 年 9 月 2 日,一位名叫艾兰的叙利亚难民儿童尸体在土耳其的海滩上被发现,并被拍下了一张影响深远的照片。

如果你在网络上尤其是中文网络上查阅这张照片的新闻时,会发现有相当一部分人在拿出各种“证据”试图证明它是摆拍的。

这张照片之所以牵动人心是因为它叙述了一个真实存在的现象是,叙利亚儿童正在因为逃难而失去生命。这个现象是真实存在的,你在这个海滩上站到深夜就能看到这个客观事实,但是具体到这个照片与这个事实是否是百分百匹配的,确实有很多可被质疑的空间,例如这个儿童有可能来自伊拉克,例如摄影师有可能把他换了一个姿势来拍摄。但是这些真的重要吗?或许对于历史学家来说,的确重要,但是对于一个普通民众来说,或许并没有那么重要,至少在此时此刻有比质疑更加重要的事情。

什么是事实,什么是真实?真实并不代表每一件事情的细节都是毫厘不差的事实,这就好比父母口中关于孩子的童年故事多少会有些添油加醋,锦上添花的成分在,但这并不能否定这些故事是真实的。

相反,由事实组成的故事也不见得一定是真实的故事。在叙利亚战争期间,一定找得到吃的好睡得好的地区,但如果一个驻叙利亚记者去这些地区报道来反映叙利亚战争期间人民的生活,这一定不是真实的报道。

人类的故事要能够被传播,或多或少总会被掺入一些夸大的成分,即便最开始的当事人只是客观陈述,也防不住他人在传播的过程中进行二次创造,最后舆论市场会自发地选择出一个兼顾了真实与传染力的版本成为我们今天耳熟能详的故事。

对于那些想要左右人民情感,篡改人民记忆的人来说,事实是他们攻击一切故事的强大武器。民众无法凭借一己之力去探寻真正的事实,只要少数人掌握了调查事实的权利,就掌握了提供唯一合法叙事的权利。他们用事实消灭一切他们所不愿意看到的故事,让这个世界只留下那些由他们控制的事实所构建出来的官方叙事。在这个叙事里,既没有人民的情感,也没有人民的记忆,只有一种强大到淹没所有人的意志。

这套叙事方式在过去被用在了欧洲难民危机中,用在了香港运动中,用在了美国大选中,用在了上海疫情中。它的强大之处在于,身处其中的人会发现自己无法与这个叙事机器所搏斗,因为它所描述的确实是事实,确实是我们所相信的故事的弱点,确实是我们作为一个平凡个体的缺陷。我们无法让自己,让自己所团结的群体,永远地做到客观,做到公正,做到实事求是,因为我们是人而非机器,而那些试图否定我们的人,只要做到一次客观,做到一次公正,做到一次实事求是,就认为可以全盘否定我们所实现过的一切努力。

我们身处于一个混乱的世界,Fake News 与 True Story 同时存在,True News 与 Fake Story 同时存在。孟姜女或许并没有哭长城,摩西或许并不能劈开红海。但又或许所有我们所相信的 True Story 并不是由事实产生的结果,而是因为有千千万万痛苦的民众,希望长城倒塌,希望抵达应许之地,所以才诞生的孟姜女,诞生的摩西,他们也许都不是事实,但他们远比事实更为强大。

少数价值

2022年4月19日 08:00

如果正在阅读这篇文章的你认为自己的价值观在这片土地上属于多数价值,被这块土地充分地实现,希望你不要继续阅读下去,也不要将它传播给任何你的朋友。我尊重你的价值观,但恐怕你不一定会尊重我的,为了你好也为了我好,请不要与我有任何交集。

我并不认为自己的价值观一定正确,但是我的价值观,是通过一点一滴的阅读,通过与不同人群的交流,亲眼所见亲耳所听构建出来的。我对它的自信源自于它一次次在苏格拉底,在耶稣,在马丁路德金,在苏轼,在鲁迅,在我身边所有优秀的朋友身上一次次被验证被歌颂,它不见得一定是全人类共同拥有和应该拥有的,但它表达了那部分我愿意与其共处的人类千百年来共同的价值诉求。

我 1995 年出生,所经历的教育完全是经过中国人民共和国教育部批准,合法合规,符合社会主义价值观的中国式教育,在我整段学生生涯中,我一直认为我的价值观是这个社会里的主流价值观,也是这个教育系统希望我拥有的价值观。在我受教育的年代,我们曾经和国家共同相信民主是未来的发展方向,共同相信侵略是反人类行为,共同相信全球化是历史的潮流,共同相信恐怖主义是要被谴责的行为,共同相信市场经济带给了中国空前的繁荣。当时的我们虽不认为我们国家已经实现了这些价值,但是绝大部分人包括国家机器都认同我们确实享有这样的价值观,而争歧无非是如何实现这些价值观,何时应当去按这些价值观践行。

但是不知道从什么时候开始,这些原本我们认为的多数价值,渐渐地变成了社会中的少数价值。虽然我们在社会主义核心价值观中明确写了民主与自由,但却在每天批判西方的民主与西方的自由。虽然我们把八年抗日战争延长到了十四年,但却破天荒地认为只要从自身利益出发,哪怕是侵略也是可以被合法化甚至共情的。虽然我们今天的经济成就完全仰赖全球化与市场经济,却可以每天大肆与几乎所有西方国家对骂并强力干预市场经济。

这些变化并不是由某一个人,或者某八千万人造成的,而是在十四亿人包括我自己共同的努力下,车轮渐渐驶到了如今这步田地。

我无法解释为什么我们会走到今天,但我确信其中肯定有我自己的一份力。我曾经认为自我实现在于努力赚钱买房成为大城市中产阶级,我曾经认为只要经济持续前进上层建筑会自然而然变好,我曾经认为多元化的世界确实应该对一些共同价值的定义有各自表述的权力,我曾经认为下一代人会比我们更加推崇自由与民主看到更大更不一样的世界,我曾经认为一个勤劳的民族辅之以不断修正的制度迟早会重回当年历史上的地位。但是直到走到今天这一步,我才知道自己错了,大错特错。

比认识到错误更加悲哀的是,这段变化正是发生在我的青年时期,发生在我最应该意识到问题,最应该去解决问题至少把问题说出来的年纪。让悲哀更加悲哀的是,我同与我共享相同价值观的人,是亲眼目睹一个个违背我们价值观的事件日复一日发生,而我们选择了冷眼旁观或是热眼嘲笑。直到今天,我们的价值观甚至成为了不可言说的价值观。

我不再愿意和任何人辩论,甚至不再愿意接受不同的观点。如果苏格拉底是错的,如果鲁迅是错的,如果启蒙运动是错的,如果独立宣言是错的,那我愿意带着这些古老的错误直到死去,也不愿意再去用新的理论,新的叙事,重新学习新时代的多数价值。如果他们是对的,如果他们会笼罩我一生,我也认了,我宁愿一生成为一个碌碌无为的错误,也不愿再推翻我过去的信仰成为一个前途光明的正确。

我失去了语言,也失去了使用语言的勇气,只能在评论区给我的同类点赞,在黑色幽默里寻找光明的踪迹,在一篇篇即将被删除的文章里获取慰藉。赛博空间里充斥着正确,让错误无处可逃,只有远处时不时传来的一声声尖叫。

RPC 漫谈: 连接问题

2021年5月6日 08:00

什么是连接

在物理世界并不存在连接这么一说,数据转换为光/电信号后,从一台机器发往另一台机器,中间设备通过信号解析出目的信息来确定如何转发包。我们日常所谓的「连接」纯粹是一个人为抽象的概念,目的是将传输进来的无状态数据通过某个固定字段作为标识,分类为不同有状态会话,从而方便在传输层去实现一些依赖状态的事情。

以 TCP 为例,一开始的三次握手用来在双方确认一个初始序列号(Initial Sequence Numbers,ISN),这个 ISN 标志了一个 TCP 会话,并且这个会话有一个独占的五元组(源 IP 地址,源端口,目的 IP 地址,目的端口,传输层协议)。在物理意义上,一个 TCP 会话等价于通往某一个服务器的相对固定路线(即固定的中间物理设备集合),正是由于这样,我们去针对每个 TCP 会话进行有状态的拥塞控制等操作才是有意义的。

连接的开销

我们常常听到运维会说某台机器连接太多所以出现了服务抖动,大多数时候我们会接受这个说法然后去尝试降低连接数。然而我们很少去思考一个问题,在一个服务连接数过多的时候,机器上的 CPU,内存,网卡往往都有大量的空余资源,为什么还会抖动?维护一个连接的具体开销是哪些?

内存开销:

TCP 协议栈一般由操作系统实现,因为连接是有状态对,所以操作系统需要在内存中保存这个会话信息,这个内存开销每个连接大概 4kb 不到。

文件描述符占用:

在 Linux 视角中,每个连接都是一个文件,都会占用一个文件描述符。文件描述符所占用的内存已经计算在上面的内存开销中,但操作系统为了保护其自身的稳定性和安全性,会限制整个系统内以及每个进程中可被同时打开的最大文件描述符数:

# 机器配置: Linux 1 核 1 GB

$ cat /proc/sys/fs/file-max
97292

$ ulimit -n
1024

上面的设置表示整个操作系统最多能同时打开 97292 个文件,每个进程最多同时打开 1024 个文件。

严格来说文件描述符根本算不上是一个资源,真正的资源是内存。如果你有明确的需要,完全可以通过设置一个极大值,让所有应用绕开这个限制。

线程开销:

有一些较老的 Server 实现采用的还是为每个连接独占(新建或从连接池中获取)一个线程提供服务的方式,对于这类服务来说,除了连接本身占用的外,还有线程的固定内存开销:

# 机器配置: Linux 1 核 1 GB

# 操作系统最大线程数
$ cat /proc/sys/kernel/threads-max
7619

# 操作系统单进程最大线程数,undef 表示未限制
$ cat /usr/include/bits/local_lim.h
/* We have no predefined limit on the number of threads. */
#undef PTHREAD_THREADS_MAX

# 单个线程栈默认大小,单位为 KB
$ ulimit -s
8192

在上面这台机器里,允许创建的线程数一方面受操作系统自身设定值限制,一方面也受内存大小限制。由于 1024MB / 8MB = 128 > 7619 , 所以这台机器中能够创建的最大线程数为 128。如果 Server 采用一个线程一个连接,那么这时 Server 同时最多也只能够为 128 个连接提供服务。

RPC 漫谈:序列化问题

2021年4月30日 08:00

何为序列

对于计算机而言,一切数据皆为二进制序列。但编程人员为了以人类可读可控的形式处理这些二进制数据,于是发明了数据类型和结构的概念,数据类型用以标注一段二进制数据的解析方式,数据结构用以标注多段(连续/不连续)二进制数据的组织方式。

例如以下程序结构体:

type User struct {
 Name string
 Email string
}

Name 和 Email 分别表示两块独立(或连续,或不连续)的内存空间(数据),结构体变量本身也有一个内存地址。

在单进程中,我们可以通过分享该结构体地址来交换数据。但如果要将该数据通过网络传输给其他机器的进程,我们需要现将该 User 对象中不同的内存空间,编码成一段连续二进制表示,此即为「序列化」。而对端机器收到了该二进制流以后,还需要能够认出该数据为 User 对象,解析为程序内部表示,此即为「反序列化」。

序列化和反序列化,就是将同一份数据,在人的视角和机器的视角之间相互转换。

序列化过程

定义接口描述(IDL)

为了传递数据描述信息,同时也为了多人协作的规范,我们一般会将描述信息定义在一个由 IDL(Interface Description Languages) 编写的定义文件中,例如下面这个 Protobuf 的 IDL 定义:

message User {
 string name = 1;
 string email = 2;
}

生成 Stub 代码

无论使用什么样的序列化方法,最终的目的是要变成程序中里的一个对象,虽然序列化方法往往是语言无关的,但这段将内存空间与程序内部表示(如 struct/class)相绑定的过程却是语言相关的,所以很多序列化库才会需要提供对应的编译器,将 IDL 文件编译成目标语言的 Stub 代码。

Stub 代码内容一般分为两块:

  1. 类型结构体生成(即目标语言的 Struct[Golang]/Class[Java] )
  2. 序列化/反序列化代码生成(将二进制流与目标语言结构体相转换)

下面是一段 Thrift 生成的的序列化 Stub 代码:

type User struct {
 Name string `thrift:"name,1" db:"name" json:"name"`
 Email string `thrift:"email,2" db:"email" json:"email"`
}

//写入 User struct
func (p *User) Write(oprot thrift.TProtocol) error {
 if err := oprot.WriteStructBegin("User"); err != nil {
 return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) }
 if p != nil {
 if err := p.writeField1(oprot); err != nil { return err }
 if err := p.writeField2(oprot); err != nil { return err }
 }
 if err := oprot.WriteFieldStop(); err != nil {
 return thrift.PrependError("write field stop error: ", err) }
 if err := oprot.WriteStructEnd(); err != nil {
 return thrift.PrependError("write struct stop error: ", err) }
 return nil
}

// 写入 name 字段
func (p *User) writeField1(oprot thrift.TProtocol) (err error) {
 if err := oprot.WriteFieldBegin("name", thrift.STRING, 1); err != nil {
 return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:name: ", p), err) }
 if err := oprot.WriteString(string(p.Name)); err != nil {
 return thrift.PrependError(fmt.Sprintf("%T.name (1) field write error: ", p), err) }
 if err := oprot.WriteFieldEnd(); err != nil {
 return thrift.PrependError(fmt.Sprintf("%T write field end error 1:name: ", p), err) }
 return err
}

// 写入 email 字段
func (p *User) writeField2(oprot thrift.TProtocol) (err error) {
 if err := oprot.WriteFieldBegin("email", thrift.STRING, 2); err != nil {
 return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:email: ", p), err) }
 if err := oprot.WriteString(string(p.Email)); err != nil {
 return thrift.PrependError(fmt.Sprintf("%T.email (2) field write error: ", p), err) }
 if err := oprot.WriteFieldEnd(); err != nil {
 return thrift.PrependError(fmt.Sprintf("%T write field end error 2:email: ", p), err) }
 return err
}

可以看到,为了把 User 对象给序列化成二进制,它 hard code 了整个结构体在内存中的组织方式和顺序,并且分别对每个字段去做强制类型转换。如果我们新增了一个字段,就需要重新编译 Stub 代码并要求所有 Client 进行升级更新(当然不需要用到新字段可以不用更新)。反序列化的步骤也是类似。

RPC 漫谈: 限流问题

2021年4月23日 08:00

微服务之间的 RPC 调用往往会使用到限流功能,但是很多时候我们都是用很简单的限流策略,亦或是工程师拍脑袋定一个限流值。

这篇文章主要讨论在 RPC 限流中,当前存在的问题和可能的解决思路。

为什么需要限流

避免连锁崩溃

一个服务即便进行过压测,但当真实运行到线上时,其收到的请求流量以及能够负载的流量是不固定的,如果服务自身没有一个自我保护机制,当流量超过预计的负载后,会将这部分负载传递给该服务的下游,造成连锁反应甚至雪崩。

提供可靠的响应时间

服务调用方一般都设有超时时间,如果一个服务由于拥塞,导致响应时间都处于超时状态,那么即便服务最终正确提供了响应,对于 Client 来说也完全没有意义。

一个服务对于调用方提供的承诺既包含了响应的结果,也包含了响应的时间。限流能够让服务自身通过主动丢弃负载能力外的流量,以达到在额定负载能力下,依然能够维持有效的响应效率。

传统方案

漏斗

优点:

  • 能够强制限制出口流量速率

缺点:

  • 无法适应突发性流量

令牌桶

优点:

  • 在统计上维持一个特定的平均速度
  • 在局部允许短暂突发性流量通过

存在的问题

在两类传统方案中,都需要去指定一个固定值用以标明服务所能够接受的负载,但在现代的微服务架构中,一个服务的负载能力往往是会不断变化的,有以下几个常见的原因:

  • 随着新增代码性能变化而变化
  • 随着服务依赖的下游性能变化而变化
  • 随着服务部署的机器(CPU/磁盘)性能变化而变化
  • 随着服务部署的节点数变化而变化
  • 随着业务需求变化而变化
  • 随着一天时间段变化而变化

通过人工声明一个服务允许的负载值,即便这个值是维护在配置中心可以动态变化,但依然是不可持续维护的,况且该值具体设置多少也极度依赖于人的个人经验和判断。甚至人自身的小心思也会被带入到这个值的选择中,例如 Server 会保守估计自己的能力,Client 会过多声明自己的需求,长期以往会导致最终的人为设定值脱离了实际情况。

什么是服务负载

当我们向一个服务发起请求时,我们关心的无外乎两点:

  • 服务能够支撑的同时并发请求数
  • 服务的响应时间

并发请求数

对于 Server 而言,有几个指标常常容易搞混:

  • 当前连接数
  • 当前接受的请求数
  • 当前正在并发处理的请求数
  • QPS

连接数和请求数是 1:N 的关系。在现代 Server 的实现中,连接本身消耗的服务器资源已经非常少了(例如 Java Netty 实现,Go Net 实现等),而且一般对内网的服务而言,多路复用时,请求数变多也并不一定会导致连接数变多。

有些 Server 出于流量整形角度的考虑,并不一定会在收到请求以后,立马交给 Server 响应函数处理,而是先加入到一个队列中,等 Server 有闲置 Worker 后再去执行。所以这里就存在两类请求:接受的请求与正在处理的请求。

而 QPS 是一个统计指标,仅仅只表示每秒通过了多少请求。

科学,技术与工程

2021年3月9日 08:00

作为软件工程师,我们在谈论自己时,总会认为自己是所谓的高科技行业从业者,但是如果观察自己的日常工作,时常会觉得似乎和真正的科技也没有什么关系。所以究竟什么是科技?我们要如何来定义我们每天的工作?

我认为宏观意义上的科技可以被拆解成三个概念:科学(Science),技术(Technology)和工程(Engineering)。

科学是观察客观世界以发现既有的自然规律,技术是组合自然规律以发明新的改造客观世界的方法,工程是发挥技术的能力以合乎客观世界要求的方去改造它。

欧姆定律,麦克斯韦方程组是科学的发现,整个电子产业的基石全在于控制电子的运动与电场的传递,电子的运动建构了存储和计算的能力,电场的传递建构了通信能力。电子的运动一定会遵守欧姆定律,电场的传递一定会遵循麦克斯韦方程组。

在自然规律的基础上,人们将电磁感应与过去的蒸汽机技术相结合发明了发电机技术。如果宇宙仅有地球存在智慧生物,那么第一台发电机的发明是真正创造了一个过去不曾有过的事物。但发电机的发明者不见得一定要制作出一个性能优异的实际发电机产品,可以只是一个原型,甚至可以只是提出了一种概念。冯诺伊曼也并没有实际去动手做出一个以他名字命名的结构下的计算机,但他的确发明了现代存储程序型计算机的概念。

要将一个抽象的计算机设计落地成为一个实际可用的计算机设备中间还是有很长一段距离,需要考虑非常多复杂的情况,例如成本,规格,安全性等。工程所做的,就是发挥实验室中的技术到非理想的现实环境中去。我们经常说的某个技术不具备可行性,通常其实是指这个技术本身虽然可行,但是在工程上不可行。

客观世界的复杂性根源来自事物彼此之间是有相互作用和联系的,改造客观世界的方法也在于利用事物的这一特点加以「组合」。自然规律是对元素周期表的组合而产生的现象,技术是在组合自然规律创造新的方法,而工程又是在组合各类技术创造出实际的产品。

人类的大脑无法并行地去进行多条件下的逻辑思考,所以往往会先剔除所有其他因素,假设一个理想的环境,从而得以专注于在此环境下得出仅适用于该环境下的某种规律,并美其名曰科学的「简洁性」。从事科学研究的人把对于非理想环境的思考移交给了去实际发明技术的人,而从事技术发明的人同样会倾向于简化问题,将复杂性移交给了那些真正去落地的工程师。这就是工程为什么会如此复杂的原因所在,他本身的目的就是去管理复杂性

在工程学领域中,软件工程又可能是其中最复杂一类工程。其根本性原因在于软件开发是一门个体创造性太强的工程。今天如果你要去制作一部手机,在供应商和成本的制约下,会让你在工程上并没有太多选择,这也是为什么所有手机厂商制造的手机参数和外观都差不多的原因。而软件就不同了,每个公司首先编程语言就可以有不同的选择,其次不同的发展阶段也需要有不同的软件架构,再者还可以有各种第三方库的选型。同样是一个业务需求,让100个工程师去实现,可能会有100种不同的技术选型组合,实际到编码层面又有更为不同的风格和设计差异。最致命的问题是,这100种做法很可能都同时是 make sense 的方案。

软件相比于硬件还有一个特点是其生命周期极长。人或许会几年换一次硬件,无论是电脑还是汽车,但有可能几十年就用同一种软件。与此同时不同软件之间因为需要彼此交互,还有着固定的接口和协议,所以软件的升级和更换还存在一个兼容性的问题。说到这里,或许有人会发现软件和人类自身是极为相似的。

1968年北约首次定义了软件工程的概念,而这一年苏联入侵了捷克斯洛伐克,中国正在大搞文革。人类浪费了大量时间在处理各种没有任何意义的内部外部矛盾,如果人类的上层也有一个「人件开发者」,那么他的架构能力肯定非常一般,但同时也非常厉害,至少能让如此混乱的人类存活几万年到今天。软件工程内部的矛盾亦如人类社会的矛盾,甚至有时人类社会自身的政治也会被带入到软件内。我们可以在 iOS 和 Android 的设计里看到共和党和民主党的影子。大量的软件工程师花费了大量的生命仅仅就为了让某个软件同时能够跑在多种设备上,但如果这些设备能够一开始就使用同一种接口,可能就没有这么多事情。软件也不是没有做此类的事情,大量的软件协议就是为此而生,这有点接近于国家间的贸易协定,制定协议的工程师就如何现实里的政客。

所以回到文章开头的问题,为什么软件明明是人类至今最高科技文明的产物,而作为软件工程师的我们却很难从日常工作中感受到高科技的工作氛围?因为今天我们编写的程序,和第一台计算机上的纸带其实没有任何本质区别,而我们日常工作所在解决的复杂性问题,恰恰是由于前人在试图解决复杂性问题时所创造的复杂性,而这其实和计算机科学本身没有任何关系。如果软件的复杂性能够被彻底消除,人类社会也就不会存在如此多的政治矛盾,但亦如同复杂性造就了人类文明的璀璨一样,正是高创造性的软件开发才能爆发出今天我们所见到的精彩纷呈的软件革命。

❌
❌