注:本文起始语“ヒーロが必要なんだよ,金城君。ヒーローが必要なんだ!”出自《迪迦.奥特曼》第49话,ウルトラの星/奥特之星(监督 原田昌树/満田かずほ, 脚本:上原正三)
注注:我本来想用“ウルトラマン、そんなに地球人が好きになったのか” 这句的(奥特曼,你就这么喜欢地球人吗?),出自《奥特曼》第39话さらばウルトラマン/再见奥特曼(监督:圆谷一,脚本:金城哲夫)。
奥特曼系列的第一话是1966年7月17日的 ウルトラ作戦第一號/奥特曼作战第一号, 但是如果从《奥特Q》开始算的话,1966年1月2日的 打倒哥美斯!则是奥特系列的起点。在从1966年到现在的这半个多世纪的时间里,日本社会和整个人类社会也是可以说风云变幻。奥特曼系列不可避免的在这个过程中,受社会思潮的影响。不同的监督和脚本试图讲述不同的故事,这也造就了奥特曼系列独特的气质
本文大概会从三个方面去聊聊奥特曼中很有趣的细节
对现实的映射其实一直是各种文艺作品经典的套路。奥特曼其实也不避免。这一点在奥特曼前期四部作品中,现实映射的气质格外明显(也就是昭和四老,初代,赛文,归曼,艾斯)。这四部作品算是奠定了奥特系列现实讨论的基调。如果说概括来分的话,大体可以分为这样几类
我会挨个挑一些很有趣的细节出来聊
奥特曼里国际问题的讨论其实某种意义是对于美苏争霸这种时代大势的一种反馈,这里面代表作的两话分别是作为昭和问题三大作之一的 故郷は地球/故乡是地球 以及赛文奥特曼第26话 超兵器R1号/超兵器R1号
先聊聊 故郷は地球/故乡是地球 这一话。剧情是这样:
在东京召开和平峰会之际,各国代表乘坐的飞机突然失事。科特队受命调查。发现了一艘隐形的飞船,以及飞船里的怪兽贾米拉。
在第一轮击退怪兽后,来自巴黎总部的艾伦解释了这个怪兽的来历:“諸君、あれは怪獣なのではありません。あれは……いや、彼は我々と同じ人間なのです/各位,那不是怪物,实际上是……不,他和我们一样是人类”
在美苏争霸期间,某个国家向太空发射了飞船,飞船失事后,迫降在了一个没有水和空气的星球上。在最恶劣的环境中,宇航员被迫”进化“成怪兽,在改造自己的飞船后,回到地球复仇
在知道前因后果后,科特队员一度失去了战意,但是总部的命令却非常残酷”ジャミラの正体を明かすことなく、宇宙から来た一匹の怪獣として葬り去れ!/不要揭露贾米拉的真实身份,将他作为一只来自宇宙的怪兽埋葬!”
最后在科特队员的努力下,贾米拉被击败。在结尾,井手队员站在贾米拉的纪念碑前,看着上面的文字“A JAMILA (1960-1993) ICI DORT CE GUERRIER QUI S’EST SACRIFIE EN QUETE D’IDEAL POUR L’HUMANITE AINSI QUE POUR LE PROGRES SCIENTIFIQUE”(贾米拉(1960-1993)这里长眠着一个为了人类和科学进步而献身的战士)说出了传世经典:
犠牲者はいつもこうだ。文句だけは美しいけれど/对被牺牲者都这样,挑一些场面上的赞美性的漂亮说辞罢了。
这一话除去奥特曼的打斗情节,整个文戏流畅而令人隐形深刻。人性的丑陋,冷战时的残酷在监督的手下表现的凌厉而又真实。顺带一提这一话的监督实相寺昭雄和脚本佐佐木守也贡献了奥特赛文第十二话这一传世经典
而我觉得代表作的另外一话是赛文奥特曼第26话 超兵器R1号/超兵器R1号。剧情是这样,奥特警备队所属的地球防卫军创造了一种全新的武器 R1, 队员们正在讨论这种武器的正当性,团和古桥队员之间发生了传世经典的对话
团:参謀にお願いしてきます、実験の中止を!/我去找参谋长,请求停止实验!
古桥:いや、忘れるなダン、地球は狙われているんだ。今の我々の力では守りきれないような強大な侵略者がきっと現れる。その時のために··/不,别忘了,丹,地球正被瞄准着。现在我们的力量是无法守住的,一定会出现我们无法抵挡的强大侵略者。为了那个时候···
团: 超兵器が必要なんですね/需要超兵器吗
古桥: 決まっているじゃないか!/这还用说吗!
团: 侵略者は、超兵器に対抗してもっと強烈な破壊兵器を作りますよ!/侵略者会制造更强大的破坏兵器来对抗超兵器!
古桥: 我々は、それよりも強力な兵器をまた作ればいいじゃないか!/我们也制造更强大的兵器不就行了吗!
团: それは、血を吐きながら続ける···、悲しいマラソンですよ/那就像是,一边吐血一边继续奔跑的,悲哀的马拉松。
吐血马拉松其实是军备竞赛很直接的注解,很生动,也很形象。在这一话中,结局也非常的令人伤感,基耶龙星/ギエロン星 作为 R1 的实验地被人类摧毁,而基耶龙星兽作为整个星球最后的遗民飞往地球复仇,最后在赛文奥特曼的头镖下咽下了最后一口气。这一切到底是谁的错误?这也是脚本想留下来让人思考的问题吧
实际上流血马拉松也一直延续到了赛文90年代 OVA 三部曲中,面对何志参谋说出的
フルハシ参謀…もう貴方に遠虑することはない。歴史が証明してくれます…私が正しかったことを。太陽系の各惑星に前線基地を置き…侵略の可能性のある星に先手を撃つ。それが地球の平和を守る手段です。/古桥参谋,我已不必再跟您客气了。历史将证明,我才是正确的。在太阳系的各个行星建设前线基地,遇到有侵略可能性的星球便先发制人。这才是保护地球和平的手段。
歴史をひもとけば分かるはずだ。二つの文明が遭遇した時、必ず高度な文明が、もう一つの文明を滅亡させる。地球だけではなく、この法則は宇宙にも当て嵌まるんだよ。/翻开历史便能明白,当两大文明遭遇时,一定是发达的文明灭绝另一个文明。这个法则不仅适用于地球,也适用于宇宙。
团也不由得再次感叹 “人類はまだ続けているのか……血を吐きながら続ける悲しいマラソンを……/人类还在继续着,一边吐血一边奔跑的可悲的马拉松。”
这种黑暗森林的思路是对,是错,从立场不同的人来看答案都是不一样的。但是从如今的视角来说,可能我觉得迪迦奥特曼开头的旁白更能适合这一章节的结束
21世紀初頭、憎しみや争い事は減り、自然は美しさを取り戻そうとしていた。この星に生きるすべてのものの願い、平和がようやく叶えられようとしていた/21世纪初,仇恨和争斗减少,自然也开始恢复美丽。这个星球上生活的所有生物的愿望,和平终于要实现了。
实际上原住民问题的讨论一直是奥特历史中很浓墨众彩的一笔,所以我选择单独把这一章从日本国内问题中分离出来。
在继续讨论前我们需要了解一点日本的国内的特殊情况。日本主要的民族冲突为两点
这两点其实在其余日本文艺作品中也有所提现。举两个例子,在动画 《New Game》 中,程序部门的主管为 (阿波根 うみこ/阿波根海子)(姓氏假名写法为:あはごん),她很讨厌别人以姓称呼自己(以为耻)(这个姓氏非日本本土姓)。另外一个典型例子是《银之匙》中的女主御影 アキ/御影亚纪,她有无意间说本土方言的习惯,但是会在不小心说方言后表现出莫名的耻感(日本人特殊的耻感爆棚)
回到奥特曼本身,昭和问题三大作中有两作是在讨论原住民问题,分别是赛文奥特曼第42话 ノンマルトの使者/农马尔特的使者,以及归曼第33话怪獣使いと少年/怪兽使者与少年。实际上早在初代奥特曼第33话 禁じられた言葉/被禁止的语言 中,身为冲绳人的脚本金城哲夫(金城和前面所聊到的 阿松波一样便借着美菲拉斯星人之口问出了 “黙れ、ウルトラマン!貴様は宇宙人なのか?人間なのか?/闭嘴,奥特曼!你到底是宇宙人还是人类?”(这里实际上是暗问:“你是日本人,还是冲绳人”),而早田的回答“両方さ/两者都是”是否也是金城自己想说的一个答案。
实际上金城哲夫的思考并不仅限于此,在看到美军从自己家乡起飞轰炸越南的飞机后,在思考自己作为冲绳人在日本所处的尴尬地位,然后有了不朽的经典 ノンマルトの使者/农马尔特的使者。在本作中,脚本陛下设定了这样一个架空场景,人类其实并不是地球的原住民,而农马尔特人才是。在大约15000年前,农马尔特人被人类赶往了海底的世界。而在本作中,农马尔特人仅存的生存地再一次的被人类所侵犯。他们放出了自己的守护怪兽。但是在赛文奥特曼将怪兽击败后,农马尔特人仅存的生存地海底都市也被奥特警备队所摧毁
桐山队长在摧毁农马尔特人时所说的台词 “我々の勝利だ!海底も我々人間のものだ!/我们胜利了!海底也是我们人类的了!” 也让这一话的意味深长。奥特曼是否是正确的,而我们是否是正确的,这种疑问大概也是脚本和监督想留给大家思考的
如果说金城哲夫对于这个问题的映射还略显含蓄的话,那么作为奥特脚本里另外一位很有名的冲绳人上原正三,对于这个问题的讽刺就是无比的辛辣,归曼第33话怪獣使いと少年/怪兽使者与少年,我觉得可能是奥史上最为残酷的一次
一个来自北海道的失去双亲的孩子,在怪兽袭击之时,被梅茨星人救下。在梅茨星人身体被地球环境污染侵蚀的愈发严重之际,少年试图从地底挖出梅茨星人的飞船,想通梅茨星人一同离开地球。在这个过程中,少年因为旁人对于他一些能力的恐惧(实际上是梅茨星人为了保护少年而做出的一些行为)而被不断的欺凌,活埋,放狗咬等等。在最后,人类的恐惧最终还是杀害了对于地球毫无侵略想法的梅茨星人。但是也放出了梅茨星人之前为保护少年所封印的怪兽。这个时候,乡秀树也陷入了对于自己战斗意义的迷茫。虽说最后还是以奥特曼获胜告终,但是这剧情足以让人感到一种彻骨的寒冷
而剧中少年的设定是北海道江差的阿伊努人,而梅茨星人的名字叫作金山,作为在日朝鲜人最常用姓氏,也不由得想起关东大地震时期日本本土对于在日朝鲜人的歧视与破坏。而少年喊出的那句 “僕の生まれた所は北海道の江差だ,僕は日本人/我出生在北海道的江差,我是日本人” ,也是意味深长
编剧在剧中设定的一个场景,少年在买面包被拒后,面对面包店老板女儿追出来给他面包的时候说出了 ”同情なんてしてもらいたくないな/我不需要你的同情“,而面包店老板女儿所做出的回应 ”同情なんかしてないわ売ってあばるたけよ/我又没同情你,只是卖给你而已“ 可能是编剧想告诉我们的东西
自由的尊重是我们所有人都应该拥有的权利。
实际上奥特曼讨论日本问题一直是老传统了,其实前一章所述的原住民问题也是日本社会问题的一部分,不过被我单独拿出来讲了。而刨开老生常态的环境和反战问题之类的话题,大体可以分为几类
首先聊聊当代青年迷茫的问题。其实在奥特曼最早期的年代里1960年代末到1970年代,在当时的几个大背景下
在这样一些大背景下,日本学生运动风起云涌(请不要无关联想),这一代的日本人实际上处在一个特殊的迷茫期(大家都在迷茫.jpg),这样一种朝气且迷茫的气质在艾斯奥特曼中表现的尤甚
PS:艾斯奥特曼本身就是极为左翼的一部作品
我们可以来看一下艾斯第二十话,一位从大学退学出来环游世界的青年对着北斗星司出了这样一段话
俺が大学を飛び出してきた気持ちが/我离开大学的心情
あんたなんかには分かるもんか/你这种人是没法理解的
自由だ/是自由
解放だ/是身心解放
みんな勝手とばかりやりやがって/每个人都想随心所欲的生活
真実なんてどこにもありやいい/真实感却无处存在
一体何を信じたらいいんだ 俺たちば/我们到底应该相信什么
而在超兽出来后,面对即将被毁的船,北斗星司和和这位青年也有这样的对话
北斗: この町の人たちとあの船どどっちが大切なんだ/这个城市的人和那艘船,哪个更重要
青年: 俺には船のほうがだ/对我来说是船
青年: あんな超獣なんかにやられてたまるか/怎么能被那种超兽打败
青年: 俺は船を守るぞ/我要保护船
北斗:バカ野郎, ちっとは自分の命のことも考えろ/笨蛋,你好歹也考虑下自己的命
青年:船は俺の命なんだ/船就是我的命
在日本学运乃至武装暴动的大背景下,这段对话回看起来也是蛮有味道的
顺带一提,我很喜欢奥特系列脚本的一个原因在于,他们不会给出一个肯定的答案,或者说,相反他们也会表现出迷茫,比如说,在同一话里,身为一个军事组织的成员的北斗,也有着自己的迷茫
最後の航海が今から3年前か。ぢようど俺がタックへ入隊した頃だ。あいつが言うように。俺もこの船もその日から鎖につながれて,自由を失ってしまったんだろうか/这艘船最后的航行是在三年前。正好是我加入TAC的时候。就像他说的一样,我和这艘船从那天起就被锁链所束缚,失去了自由吗?
这可能也是时代的迷茫.jpg
而与此同时,在这样一种时代背景下,随着女性解放思潮的进一步发展,除开奥特曼形象受之影响(艾斯.jpg),脚本和监督也在尝试在剧集中进行讨论,归曼第43话 魔神 月に咆える/魔鬼下月光下咆哮 和第49话 狙われた女/被盯哨的女人 便是其中代表作(这两话脚本都是 石堂淑朗)
在43话中,伊吹队长在回家后,关于女儿美奈子有这样一段很有趣的对话
外婆:美奈子も大きくなったら マットの人のお嫁さんになるのかね/美奈子长大后会嫁给 MAT 队的人吗?
美奈子:なの嫌いよ/我才不要
伊吹: マットは私1人たくさんというわけだな/是觉得家里有一个 MAT 队员就够了把
美奈子:違うの,マットの人のお嫁さんにならないってことは,マットの隊員になるということを妨げはしないわ/不是的,不想嫁给 MAT 队员是为了不妨碍我成为 MAT 队员
美奈子妈妈:女の子のくせに/女孩子家家的
美奈子:だって丘隊員だって女/可是丘队员也是女的
伊吹:まつお前は平凡な男と結婚してくれ,そのほうがお父さんは安心だ/算了,你还是找个普通的男生结婚吧,这样我才会更放心一些
美奈子:嫌よ私はウーマンサプ派な/不要,我可是女权主义者
这一段对话,即便从今天的眼光来看,也是非常的超前的。
其实奥特曼剧作里,非常精巧的片段还很多,有些是脚本/监督来表达自己心中的想法,有些是受时代环境的影响(另外一个很典型的例子是雷欧当年受到《日本沉没》系列很深刻的影响)。限于篇幅,这里不再展开了。如果有兴趣的话,感觉我可以开个系列单篇来做单元回的解析
实际上奥特曼系列作为特摄剧,一直在孜孜不倦的追求视觉效果的形式。从初代奥特曼首次空战巴尔坦星人开始,到手绘出的艾斯的梅塔利姆光线。监督一直在尝试给观众全新的视觉体验。不过说实话,我觉得目前的奥有点漫威的感觉,过多的特效,失去了原本那种精心制作的感觉。
抛开个人碎碎念以外,我自己觉得奥特摄影的巅峰应该是在迪迦,可能熟悉的人已经猜出来我想说什么了,16话よみがえる鬼神/苏醒的鬼神,37话花。将日式美学和能剧元素发挥到了极致,一种特殊的美轮美奂
这个时候必须上图了
说实话,我觉得这种特殊的美感,简直让我一身鸡皮疙瘩
其实奥特曼的宗教气质一直非常的浓厚。其实千言万语归为一个问题
奥特曼是人还是神?
在早期的两部作品中,初代和赛文,毫无疑问的是神性非常重的化身。从更高的维度去观察人类,注视人类。初代结局佐菲问初代的“ウルトラマン、そんなに地球人が好きになったのか”,不由的让人有一种神爱世人的联想。而赛文奥特曼所设定的,深爱着人类,帮人类所承担了人类的罪恶,这种设定也是非常的神性
我们若认自己的罪,神是信实的,是公义的,必要赦免我们的罪,洗净我们一切的不义。 — 《新约》约翰一书1:9
而从归曼开始,人性的部分开始融入到奥特曼的内核中,奥特曼会因为人间体至爱被外星人杀害而陷入极端愤怒,而人间体也会因为目睹人类的黑暗而质疑战斗的意义
到了迪迦,可能居间惠队长的一段话,为这个长达20余年的争论画下了一个暂时的休止符
最初にウルトラマンをこの目で見たとき私は神に出会えたと思った。人類を正しい方向に導いてくれる存在だと。でも違うのよね。それがだんだん分かってきたの。ウルトラマンは光であり人なのね。/我最初见到奥特曼时,以为遇到了神,以为他是将人类指引往正确道路的存在。但是我错了,后来我才渐渐明白,奥特曼既是光,也是人。
奥特曼走到如今,已经过去了半个世纪。在这半个世纪中,不同的脚本和监督一起给无数的人绘制了一个梦幻的世界。这也是我想用 “ヒーロが必要なんだよ,金城君。ヒーローが必要なんだ!” 来做为本文开头的原因
而怎么结束这篇文章呢
毫无疑问的还是那句话
ウルトラマン大好きだ
]]>众所周知,容器逃逸并不是什么令人稀奇的问题了(不被逃逸的容器才是稀奇),2月初,runc 社区正式公布了一个船新的逃逸 CVE,参见 https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv,版本横跨 1.0 到 1.1.11
这个 CVE 的核心特性在于“可以通过镜像分发的方式,成本很低的进行逃逸”
我们先来复现一下这个问题
我自己的环境是这样
看这篇博客的同学可以参考下面方式进行环境准备,
1 | git clone https://github.com/opencontainers/runc |
然后我们可以准备这样一个 Dockerfile
1 | FROM ubuntu |
执行 docker build . -t test
然后我们可以执行 docker run --rm -ti test bash
,需要多次才能执行成功, 执行成功后我们进入容器 shell
然后我们通过 cd ../..
退出到根目录,接着我们就能看到,我们宿主机完整的文件了。同时我们还能使用 chroot 能命令,切换到宿主机的根目录。
那么这样一个问题是怎么导致的呢?
聊这个 CVE 之前,需要聊一些背景知识。首先是 Linux 下 openat2 这个 syscall。openat2 是 openat 在 Linux 5.6 之后的一个对于原本 open/openat 的一个扩展。其核心在于可以让用户进行更细粒度的控制,包括安全控制。比如 O_CLOEXEC
(在执行 exec 时,自动更关闭之前的文件描述符)等细粒度的 flag 控制。
然后我们需要来聊聊整个容器的启动过程
容器启动的过程概述可以抽象为这样,
docker-client -> dockerd -> containerd -> containerd-shim -> runc(容器外) -> runc(容器内) -> containter-entrypoint
在启动过程中,runc 会负责设置容器的 cgroup 信息
1 | func (p *initProcess) start() (retErr error) { |
众所周知,cgroup 最常见的控制方法是直接写入 cgroup 文件,runc 也不例外,同时为了保证文件的安全性,runc 会尝试使用 openat2 来进行文件打开。但是如前面所说的一样,openat2 是个在 Linux 5.6 之后才引入的 syscall,那么咋整捏,runc 有一个特殊方法 prepareOpenat2
1 | func prepareOpenat2() error { |
眼尖的同学已经看到了,在测试是否有 openat2 的时候,runc 会使用 unix.Openat2(-1, cgroupfsDir, &unix.OpenHow{Flags: unix.O_DIRECTORY | unix.O_PATH})
这个调用来测试是否有 openat2,在这里,我们没有使用 O_CLOEXEC
,同时我已经打开的文件并没有被关闭, 这就导致了一个问题,如果系统支持 openat2,这里就会存在一个文件描述符泄漏(简单的给一个结论这里泄漏的文件描述符指向 /sys/fs/cgroup
)
而利用方式也很简单,我们上面的样例 Dockerfile 中的 WORKDIR /proc/self/fd/7/
就是利用这个泄漏的文件描述符,WORKDIR 在 OCI 中会转化成 CWD
的设置,在 runc 启动过程中,将直接通过 chdir 的方式进行设置
1 | // before executing the command inside the namespace |
那么换句话说,我们容器内启动的进程默认的 /proc/pid/cwd
就是我们设置的 /proc/self/fd/7
也就是我们宿主机的 /sys/fs/cgroup
,这就导致了我们在容器内可以直接访问宿主机的文件
这整个流程只能说,,阴差阳错
如果我们 runc 版本没有办法及时更新到修复后的版本,那么我们有没有办法探测到这个问题呢?可以
这个攻击的特征非常简单
/proc/self/fd/*
那么我们用 eBPF+Tracepoint 处理下就 OK
1 |
|
将事件上报到用户态,然后用户态用正则处理下 path 就行。当然这里的特征还能更多样化一些
由于我比较懒,所以在博客里就不写了,有兴趣的同学可以自己写写(XD
也没啥好总结的,容器逃逸不是新闻,不逃逸才是(XD
]]>这篇文章可能会有些枯燥,所以如果对此不感兴趣的同学可以直接 x 掉
在聊 Python 3.13 具体的实现之前,我们需要来了解下它所采用的 JIT 方案的基础知识
JIT 本身的定义我相信阅读这篇文章的同学已经非常了解了,所以此处不再赘述。JIT 核心分为两大块
本文主要会聊代码的生成部分
在此之前,Python 生态里一个 JIT 的实现,Pyston/Pypy,他们所采取的方案其实是和 LuaJIT 的方式类似,开发者手写汇编来完成代码的特化,然后依赖 DynASM 执行相关的代码
这种方式主要的缺陷在于
为了给大家一个直观的感受,我给出一个我之前写过的汇编的例子来作为演示
首先,我需要实现的功能很简单,用 C 来描述应该是这样的
1 | int main(int argc, char *argv[], char * envp[]) { |
因为一些尺寸极端敏感的场景,这份 C 代码没有办法直接 link libc,为了尽可能的压缩 binary size,我选择用汇编实现,以下是 X86_64 的 ASM
1 | .global _start |
同时,因为这个功能需要跨平台实现,所以我们需要同时实现 ARM64 的版本,以下是 ARM64 的 ASM
1 | .global _start |
你能发现,X86_64 的寄存器和 ARM 生态完全不一样,这就导致了我们需要为不同的平台写不同的汇编代码,你再考虑下我们需要
即便 DynASM 已经对跨平台做了一些抽象,但是直接手写汇编所带来的心智负担还是非常大的
所以,我们需要更现代化的方案,这就是今天要聊到 Copy And Patch。其核心在于利用已有编译器生成的汇编代码,然后对其进行 patch,来完成代码的特化
我们一点点来了解这个方案,首先我们从最基础的一个代码入手
假设我们现在有一个最基础的 C 代码
1 | int add(int a, int b) { |
这个代码没有任何问题,我们可以直接用 gcc 来编译它,然后反汇编,看看它的汇编代码是什么样的
1 | 0000000000000000 <add>: |
最基础的汇编代码,没有问题。
那么我们现在有这样一个场景,我们提供两个函数
这个两个函数将用于加载我们左右两个操作数,然后我们的代码变成下面这样
1 | int load_left(); |
我们开垦一下汇编
1 | 0000000000000000 <add>: |
我们关注到,汇编中有这样两行奇怪的东西
1 | e: e8 00 00 00 00 callq 0x13 <add+0x13> |
Bingo,熟悉一些基础的程序知识同学应该反应过来了,e8
指令(即 x86 下的 callq 指令)后面的 00 00 00 00
地址,将会在执行时,被 reloc 成为 load_left
和 load_right
的地址。
那么可能有些同学已经反应过来了,如果我们有办法将这段汇编代码中的 e8 00 00 00 00
替换成 e8 xx xx xx xx
,那么我们就可以在这里 patch 上我们的代码了。这里是不是可以作为我们 JIT 的入口了呢?
当然,这里有一个问题,e8
后面的指令地址应该怎么样确定呢?
这里我们可以注意到,程序中有这样的部分 000000000000000f: R_X86_64_PLT32 load_left-0x4
, 这个是一个 ELF 的 Relocation Entry,它的作用是告诉我们,e8
后面的地址,应该是 load_left
的地址,同时,我们也能知道重定向部分的起始 0x0f
.
同样的类型还有很多,比如 R_X86_64_PC32
,R_X86_64_GOTPCREL
等等,这些类型的 Relocation Entry 都可以帮助我们定位到我们需要 patch 的地址,以及帮助我们计算偏移
再举个例子
1 | // 17f: 48 bf 00 00 00 00 00 00 00 00 movabsq $0x0, %rdi |
这里我们可以看到,48 bf 00 00 00 00 00 00 00 00
和 48 be 00 00 00 00 00 00 00 00
后面都有一个 R_X86_64_64
的 Relocation Entry,这个 Relocation Entry 告诉我们,这两个指令后面的地址,应该是 .rodata.str1.1
的地址,同时,我们也能知道重定向部分的起始 0x181
和 0x18b
,这样我们就可以计算出偏移,然后 patch 上我们的代码了
那么这就是整个 copy and patch 的大概过程,我们可以利用编译器生成的汇编代码,然后通过 Relocation Entry 来定位我们需要 patch 的地址,然后 patch 上我们的代码。最终尽可能的简化我们的心智负担
Python 3.13 目前的 JIT 方案已经确定下来了,它的核心就是 Copy And Patch,现在我们整体来看一下
首先,Python 有一个 Python/executor_cases.h
文件,囊括了我们所有的字节码和对应的操作
比如
1 | case _BINARY_OP_ADD_INT: { |
然后我们新增加了一个 tools/template.c
文件,
1 |
|
其中,_JIT_OPCODE,由编译时传入,作为当前的 opcode,因为这是一个固定值,所以编译器在编译的时候,会 strip 掉其余的分支,只保留当前 opcode 的分支,某种意义上,核心的 switch 部分就编程这样了(以 _BINARY_OP_ADD_INT 为例)
1 | switch(_BINARY_OP_ADD_INT) { |
我们最终能得到这样的汇编
1 | // 0: 55 pushq %rbp |
OK,我们在编译器(目前 Python 选用的 LLVM 系列的工具链,编译器为 clang)开了 O3 编译后得到中间文件后,我们利用 llvm-objdump
和 llvm-readobj
来获取到我们需要的信息(这里其实也是一个非常棒的细节,因为我们要跨很多平台,要处理几种不同的二进制格式,比如 Linux 下 ELF,Windows 下 PE,MacOS 下 Mach-O,所以我们需要一个统一的工具来处理这些二进制格式,而 LLVM 的工具链就是这样的工具)我们能注意到,在上面的代码中,有这样一些重定向条目
1 | // 0000000000000025: R_X86_64_64 _Py_stats |
然后我们就可以根据从工具链中获取到的信息,来定位到我们需要 patch 的地址,然后生成一些运行时 patch 的 flag,最终生成这样一份 C 代码
1 | static const unsigned char _BINARY_OP_ADD_INT_code_body[306] = {0x55, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x48, 0x83, 0xec, 0x18, 0x48, 0x89, 0x54, 0x24, 0x10, 0x49, 0x89, 0xf7, 0x48, 0x89, 0x7c, 0x24, 0x08, 0x4c, 0x8b, 0x66, 0xf0, 0x48, 0x8b, 0x6e, 0xf8, 0x49, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x8b, 0x06, 0x48, 0x85, 0xc0, 0x74, 0x07, 0x48, 0xff, 0x80, 0x88, 0xa4, 0x01, 0x00, 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x89, 0xe7, 0x48, 0x89, 0xee, 0xff, 0xd0, 0x49, 0x89, 0xc5, 0xf6, 0x45, 0x03, 0x80, 0x48, 0xb9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x24, 0x49, 0x8b, 0x06, 0x48, 0x85, 0xc0, 0x74, 0x07, 0x48, 0xff, 0x80, 0x78, 0x58, 0x09, 0x00, 0x48, 0x89, 0xcb, 0xff, 0xd1, 0x48, 0x89, 0xd9, 0x48, 0xff, 0x88, 0xc8, 0x15, 0x04, 0x00, 0x48, 0xff, 0x4d, 0x00, 0x74, 0x37, 0x41, 0xf6, 0x44, 0x24, 0x03, 0x80, 0x75, 0x49, 0x49, 0x8b, 0x06, 0x48, 0x85, 0xc0, 0x74, 0x07, 0x48, 0xff, 0x80, 0x78, 0x58, 0x09, 0x00, 0xff, 0xd1, 0x48, 0xff, 0x88, 0xc8, 0x15, 0x04, 0x00, 0x49, 0xff, 0x0c, 0x24, 0x75, 0x2b, 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x89, 0xe7, 0xff, 0xd0, 0xeb, 0x1a, 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x89, 0xef, 0xff, 0xd0, 0x48, 0x89, 0xd9, 0x41, 0xf6, 0x44, 0x24, 0x03, 0x80, 0x74, 0xb7, 0x49, 0x8d, 0x47, 0xf0, 0x4d, 0x85, 0xed, 0x74, 0x2e, 0x49, 0x83, 0xc7, 0xf8, 0x4c, 0x89, 0x28, 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x7c, 0x24, 0x08, 0x4c, 0x89, 0xfe, 0x48, 0x8b, 0x54, 0x24, 0x10, 0x48, 0x83, 0xc4, 0x18, 0x5b, 0x41, 0x5c, 0x41, 0x5d, 0x41, 0x5e, 0x41, 0x5f, 0x5d, 0xff, 0xe0, 0x48, 0x8b, 0x4c, 0x24, 0x08, 0x48, 0x29, 0xc8, 0x48, 0x83, 0xc0, 0xb8, 0x48, 0xc1, 0xe8, 0x03, 0x89, 0x41, 0x40, 0x31, 0xc0, 0x48, 0x83, 0xc4, 0x18, 0x5b, 0x41, 0x5c, 0x41, 0x5d, 0x41, 0x5e, 0x41, 0x5f, 0x5d, 0xc3}; |
最终所有指令编译完成后,最终会生成 jit_stencils.h
文件,被我们其余 CPython 代码引用,编译进我们的二进制中
然后我们来看下,我们的 JIT 是如何工作的
1 | int |
这一部分代码看似很复杂,实际上核心代码很简单,利用 jit_alloc
生成一块内存,然后利用 emit
将我们的汇编代码写入到这块内存中,然后利用 mark_executable
和 mark_readable
将这块内存标记为可执行和可读,最终将这块内存的地址赋值给我们的 executor,这样我们的 executor 就可以执行我们的 JIT 代码了
然后
1 | patches[HoleValue_CODE] = (uint64_t)code; |
这一部分就是将我们提前预置的一些 flag 设定具体的值,以便后续的 patch
然后 patch 核心的部分,就是根据各平台的 LDD 规则来将我们动态的一些地址 patch 到 relocate 的位置
1 | switch (hole->kind) { |
整体上的思路就是差不多这样一些,剩下的就是一些 corner case 的处理,本文先不在展开。大家感兴趣的话,我单独开单篇再来聊一些
我们能发现 Python 3.13 JIT 方案的一个很大的特点是,尽可能的利用了 LLVM 生态的东西,编译器用 clang,编译参数开 -o3 获取最大的性能,二进制用具用 llvm-objdump
和 llvm-readelf
,这样做相较于其余方案的好处非常非常的明显
所以我说 Python 3.13 的 JIT 方案可谓是又新又好
]]>如果要说要说今年最让我记忆犹新的瞬间,那么毫无疑问是今年8月,月初的某一天,我毫无征兆的突然情绪爆发,冲到窗口边打开窗户,试图从十八楼一跃而下。不过可能我没法有游戏里的主角这一样的光环,落地,转身,拍拍屁股走人。可能只是在繁华的街道上徒留一地碎肉。
所以,妹子不知道为啥发觉了我的异常,在我一只脚迈出窗外的时候,死命将我拉了回来。我从没想过她的力气会那么大,会那样的无畏的拉着我。
所以我有些闲暇坐在这,写下这篇文章。
从试图跳楼往前回溯,是连续几周的同一个噩梦,梦回到了自己被强奸的现场,每一次都是同样的真实。可能我想我的一些坚守的防线在不知不觉中被打破了吧。
如果说2023的关键词第一个是爱,那么第二个应该就是 tough 了
无数的噩梦,自我的怀疑,各种不如意的琐事,最喜爱的演员的离世,最惨的时候两周去了六次急诊缝了5针,打了三针,这一些 tough ,负面的词一直环绕着我。某种意义上今年是我内心猛兽更被释放的一年。我某种意义上一度陷落十来岁那种暴戾的状态,所幸, 我挺了过来。
抛开那些 tough 的内容,这一年,其实也是蛮有希望的
最大的变化是我们家迎来了一位新的成员,边牧林克,一个让你操心不已,但是却又让你想起来很有感觉的小狗。他在学会使用说话按钮最喜欢表达的一个词就是 “Loving You”。是不是非常的暖!(当然林克的操心程度非常的大,包括不仅限于撕了我概率论的书,啃了我不少电子产品,没事偷吃猫粮导致拉肚子(但是我们依然还是一只可爱的小狗
而另外一个变化是,我们家迎来了一位特殊的成员,一只叫小熊的小猫,或者是老猫。小熊是一只流浪猫,在夏日的某一个夜晚和我相遇,当时的我和妹子在因为家里已经有过多的猫而纠结时,我们说,如果我叫他,他过来,我们就救。话音未落,小熊便打着呼噜过来了,毫无防备。那么就救吧。
小熊是一只脾气异常好的猫,在住院时便成为医院的医护人员的心头宝,他也是一只异常努力的猫。努力的活着,异常努力的活着,他在我们遇见他时状态便很不好,重度口炎+肾衰,这对于任何生物来说都是异常痛苦的。但是小熊成功的挺了过来,现在状态非常稳定。回家后,小熊最喜欢躺的地方是我枕边,听着他打呼噜的时候,我在想某种意义上我在想小熊和我相遇某种意义上算是命中注定,互相拯救。
说到这个,突然想起今年一件让我开心很久的事,我一直捐助的学生,今年非常顺利的考上了大学,看着她顺利走出大山时候的笑容,我觉得我捐助的钱花的非常值得。“教育是最好的公益”
当然我 2023 年也还折腾了很多东西
差不多是这样,如果说 2023 年是足够 tough 的一年,那么被爱与希望环绕着的我,也好歹算是走了出来
感情继续进入了第五个年头,可能还会有剩下很多个年头
只是没有想到,今年突然给这一份感情加上了不少的厚重感,荆澈同学现在是我字面意义上的救命恩人Hhhhhhh
从某个意义上来说,在生死交杂的混乱边界,很多时候我会迷失,会徘徊,但是有些时候想沉沦下去的时候,会感觉背后有一双手一直在用尽全力拽着我。当我回头时总会想起荆澈同学哭喊的那句话“你死了我咋跟你爸妈交代啊”
说点轻松的,今年的感情特殊的一点是,我们有狗之后,我们俩的分工便成为了,我负责狗的日常,她负责狗的训练。某种意义上来说我们算是体验一定程度上的无痛当爹/妈
不管怎么样,希望24年也能顺利的走下去,一定要多出去玩(23年我状态不好没咋出门,感觉对不起荆澈同学(
啊!荆澈同学今年还给我买了变身器,她让我扛不住的时候按下变身器变身成奥特曼就好了!
Gaia!(超大声
今年的技术生涯,可以总结为一句话”改革,啊不,学习进入了深水区“
是的,毫无疑问的进入了深水区,站在29岁的当下,我不由的发现,技术的学习对于我来说似乎到了一个新的瓶颈。我需要更多的去思考,去理解,去实践,去总结。而且期间有一件事对于我来说是致命的
做的事情没有办法很快的见效
是的。我现在学习,去体验很多的很多东西,他们的结果可能需要以周或者月乃至季度为单位才能看到。这对于我来说无疑是一个巨大的挑战。我在此期间会不断的进入一个焦虑,与自我怀疑的状态。质疑自己是否还能继续走下去,质疑自己是不是一个垃圾。
而这种焦虑感也会体现在我的工作中,我的 Leader 曾经对我说”我感觉你很多时候在不断的找事来做“。Exactly,某种意义上是在填补心中的空虚。换句话说,我自己让自己陷入到一个 Everythings issues go go go 的状态中。
从另外一个方面来讲,今年的一个经常被提起的话题是 AI 是否会最终让你失业。我对此倒是有很坚定的信任,在肉眼可见的时间里,人终究还是会作为可靠性的最后一道防线
聊聊自己做的一些事吧
在学习方面,今年主要是通过 CSAPP 对于计算机体系结构有了更深入的理解,包括配合对照 Linux 内核里的一些实现,对于整体的计算机体系的 sense 有了不小的提升。这一点也体现在我在社区和工作中调试一些问题的时候,我的直觉会更为准确。
另外一点我自己觉得比较好玩的是,今年机缘巧合之下,因为调试的需求,去系统的看了一些 ELF 的里调试信息 DWARF 的一些东西,这也帮助我在做一些场景下问题定位的时候,更加的准确。
在开源项目方面,今年各种项目多多少少有一些参与,大概列一些把
然后去 PyCon China 做了最后两场分享,半参与的开始恢复一些博客的录制
整体来说,今年输出还是比去年少不少的
如果说明年有什么想做的事的话,那么还挺多的
仔细想想也还挺有时不我待之感
差不多这样吧。对照了一下去年列的 OKR,今年其实有不少没有完成,但是我还是想冒昧给自己一个 3.75(XD, 毕竟我活下来了(阿里味太重了(不是
在生死交杂的世界里,唯有爱与希望是我们继续下去的动力
Per aspera, Ad astra
我很喜欢的翻译是
]]>循此苦旅,终抵星辰
熟悉我的朋友都知道,我是个 SRE,啊,不是,书接上回,大家都知道我在六月上旬的时候,家里的 HomeLab 来了一次全新的调整。
整体的效果如下
现在我大概的设备如下
同时,因为我发现 VM 管理很麻烦,所以我在10月底,将 K3S 引入了我的 Homelab 中,具体的结构如下
这样算是做了个最基本使用的环境(后续可能会再如几个 NUC 作为一些特殊的节点)
差不多 v1 状态介绍完了,那么接下来,我来介绍下 v2 的改造
我最近在做一些 Redis 迁移的工具,所以我想常态化的在集群里跑一个 Redis Cluster。基础的技术方案很简单 redis-cluster helm charts 不就完了嘛。
但是问题出现在,我想在集群外访问这个 Redis Cluster,那么这就有点麻烦了。因为自建集群存在一个问题是需要一个 External-IP 作为入口。在云厂商托管的 K8S 中,这一切都很简单。但是在自己的 Homelab 中就需要别的技术方案了。
调研了一圈,发现 MetalLb 将会是一个不错的选择。
那么就,安装一下?这里又有一个当时觉得头疼的地方,因为我用的主路由 UDM-SE 不支持 BGP,所以我只能选择 L2 模式。那么就配置一下
1 | apiVersion: metallb.io/v1beta1 |
然后我的 Service 就能成功的分配到可以访问的 IP 了。一切看起来很好对不对?很明显不是啊!
说到不足就需要来先聊一下 MetalLB 的 L2 是怎么做的。它在每个节点上都会启动一个 Speaker 的 DaemonSet,会将你 SVC 被分配的 IP 和 MAC 地址走 ARP 宣告出去(IPV6 走 NDP)。那么这样做有几个问题
这合理吗?这不合理啊。这清真吗,当然不清真啊。那么咋整啊,如果想换成 BGP 的话。
前面我说了我用的主路由 UDM-SE 不支持 BGP,所以我只能选择 L2 模式。 对吧。但是仔细思考之后,事情好像起了那么一些变化?
是这样,目前我的主路由只会作为最上层的网关,而我大部分设备的 Gateway 是通过 DHCP 下发的配置指向了我的 OpenWRT 实例,那么这样说的话,我好像在 OpenWRT 上做 BGP 支持就可以了?Exactly!
我把我定制的固件 Auto-OpenWRT 添加了 quagga
相关的包后,编译,替换虚拟机镜像。然后开始进入我们的配置流程。
先看下 OpenWRT 的配置(ssh 到 OpenWRT 上,利用 vtysh 进行配置)
1 | router bgp 65000 |
这里我将 OpenWRT 的 BGP AS 设置为 65000,然后将 K3S 的 BGP AS 设置为 65009。
然后我们对 K3S 的配置进行修改
1 | apiVersion: metallb.io/v1beta2 |
OK, 回来看下我们的 OpenWRT 的一些结果
1 | OpenWrt# show ip bgp summary |
Right,Neighbor 成功建立,然后我们创建几个 LoadBalancer SVC 看一下
1 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE |
OK 分配了一些 VIP,我们再来看下 OpenWRT 的 BGP 路由表
1 | OpenWrt# show ip bgp |
很好,我们的路由表里面已经有了我们的 VIP,以及 Next Hop,当某个节点发生故障的时候,OpenWRT 会自动将路由表更新,然后将流量转发到其他节点上。
那么 SVC 的需求算是告一段落了,这算本次升级的结束了吗?
当然不是。
在完成 BGP 后,OpenWRT 将会承担更多的任务。我现在的方式是,有一个一模一样的备机在旁边,当主机挂了的时候,我需要手动切换到备机上。这样的话,我就需要一个自动化的方案。
那么这个方案就是 VRRP。这个协议的原理很简单,就是在两台机器上都跑一个 VRRP 的 Daemon,然后通过一个 Virtual IP 来进行访问。当主机挂了的时候,备机会接管这个 Virtual IP。
在 OpenWRT 上,我们可以通过 keepalived
来实现这个功能。我们先来看下配置
1 | global_defs { |
这里我们将 Virtual IP 设置为 192.168.5.1,而背后的四个节点为
其中 192.168.5.2 为主节点,这四个 OpenWRT 都在我四个 NUC 机器上,确保一个硬件挂了其余的节点可以接管。
我们来测试下,先下线 192.168.5.2
1 | Thu Dec 28 20:14:52 2023 daemon.info Keepalived_vrrp[13785]: (VI_1) Entering MASTER STATE |
我们能看到主节点已经切换到了 192.168.5.5 上了,然后我们再上线
1 | Thu Dec 28 20:26:22 2023 daemon.info Keepalived_vrrp[3000]: (VI_1) Master received advert from 192.168.5.2 with higher priority 100, ours 10 |
我们能看到,主节点也已经切换回了
现在算是终于做到了基础的 HA 了
这次升级主要还是以软件升级为主,希望能尽可能体验和云上一致。同时保持基础的 HA
下一步的迭代计划是(我自己想的):
差不多就这样把
]]>Python 3.12 绝对是一个史诗级版本,在我心目中,它对于 Python 的意义,大于 “async/await” 的 Python 3.5 和 “Type Hint” 的 Python 3.6 对于 Python 的意义。
或者我们可以这么说在未来数年的时间里,Python 后续的很多意义重大的变更,其都能上溯到 Python 3.12。
理解我这一个观点,我们来说一下 Python 的几大痛点:
而这样一些问题,Python 3.12 上都有了极大的进步
我们下面分别来聊聊这一些变更
先聊聊 PEP 669,PEP 669 其实是受 PEP 659 启发的一个 PEP。在此之前,Python 内部事件的监控非常的困难,或者说代价极为高昂。以之前 laike9m 做的 Cyberbrain 这样一个工具来说,它能实现如下的调试效果
通过这个功能能很方便的调试函数的各种调用栈和入参。实际上这样的实现整体上基于 sys.settrace
这样一套 API 来做的。会非常的慢,原因是在于每一个栈帧的调用都会触发一次 sys.settrace
的调用,可谓是在主干道上疯狂堵塞了。
而 PEP 669 则不一样,它存在两种 level
换句话说,可以让我们根据场景来选择不同的实现粒度(按照官方的 Benchmark,比 PEP 523 快了数个数量级)
同时 PEP 669 也实现了完整的一套 Event 语义
对比一下 sys.settrace
提供的事件
什么叫又新又好啊?通过 PEP 669,我们可以很方便的去实现性能更好的调试器,低成本的对 hot path 进行调用统计等很多操作
PEP 669,先告一段落,接着继续往下聊,GH-96143,也是一个贼为重要 PR
我们都知道,对于解释性语言在旁路 trace 最难做的一个事情,就是你很难能将内存中的地址和现在执行的代码关联起来(因为直接地址是没有意义的)。而 GH-96143 则是做了一个极简化的 JIT,将 Python 的 code block 和内存地址锚定,可以让我们在旁路直接 trace 内存地址。一个效果
1 | 7f0caf8aa70c b py::_path_abspath:<frozen importlib._bootstrap_external> |
具体的实现可以参考我另外一篇文章,这里就不展开了。终于算是补全了其余语言已经出现很早的功能了
众所周知,Python 祖传的被黑的的地方是 GIL,或者说 GIL 给它带来了很多比如简化开发者代码心智负担的优势以外,也给带来额外的黑点。在降本增效,啊不对,性能提升越来越重要的今天,我们需要来尽可能的规避 GIL 所带来的性能问题
但是 GIL 不是那么好移除的,因为 Python 不少的状态都是全局状态,其设计的一部分假设就是 GIL 所带来的线程安全性,很典型的例子有
在我们继续推进 non-GIL 之前,我们需要将关键状态从全局解耦。PEP 684 其实就是起到了这样的作用。除了引入 per-interpreter 的 GIL 以外。它最重要的意义莫过于将包括 GC 状态在内的一系列关键状态,下放至 interpreter 的级别。为后续的 non-GIL 落地打下了很坚实的基础
Python C API/ABI 一直是一个老大难问题,因为一度和 Python VM 的 API 耦合的非常紧密。Python 官方也意识到了这个问题,从 Python 3.2 引入 Limited API,到 PEP 652 ,Python 一直在试图提供稳定,跨版本的 API,但是始终有一环没有闭环,那就是对于类型的处理
比如说,我们 Rust/C++ 之类的 Binding,通常可能会在 Python 标准类型的基础上进行一部分派生,中间存入自己的状态。那么我们最开始的写法是(也只能是)
1 | typedef struct { |
那么你觉得这段代码有什么问题吗?可能你发现了,这段代码,直接依赖了 Python 的 PyListObject,这样一个数据结构实际上是内部的实现细节,它的内存布局,兼容性随时可能随着版本变化而变化。所以 PEP 697 就尝试去填补上 Python Stable API/ABI 这最后一环
在 697 后,我们可以这样实现我们的代码
1 | typedef struct { |
我们能看到,我们能直接将我们想要在数据结构中扩展的部分直接 attach 到基类上,而无须关心其细节。这样一来,我们就能够在不同版本的 Python 上,保证 ABI 的兼容性了
很多人在去判断一门语言新版本的时候,会从语法糖等角度去判断,但是我自己更喜欢从它的稳定性等常常被人忽略的角度去判断。我对于 Python 3.12 的一个最直观的评价就是在可观测性,调试性,API/ABI 稳定性上,终于和其余语言站在了同一水平线上
]]>很多人觉得参与进开源社区很难,无外乎几个原因
我自己对于这个观点表示不太认可,所以我从九月中旬开始,用了一个月时间,利用 incubator-opendal 做了一个实验,为什么会选择这个项目?原因以下几点
所以我想看一下,我自己作为 fresh man 能在这个社区里面做什么事
截止到今天,我整体的提交记录如下
整体的花费的时间接近34h
整体工作内容横跨了几个方面
而截止到目前,还有很多工作需要去继续跟进,比如
大概总结一下就是通过这一个月的历练,我自己 Rust 熟练度得到了极大的提升(感谢 @Xuanwo 的不杀之恩),我自己对于整个社区的运作的理解也得到了印证。从我个人的角度来说,这次一次实验算是符合我的预期:很多时候开源没你想的那么难。
首先一点,我要纠正一个我经常见到的一个错误的观点“社区里只有那些技术难度很大,很有挑战的工作才是有价值的”。完全错误的观点,我之前和 @Xuanwo 以及 @Tison 有一个共同的观点是,很多工作并不是一开始就变得困难,而是一点点的需求形成。同时一个社区里往往存在很多关系到用户体验,必须要有人去做,但是受限于精力,已有的维护者暂时没法去做的事情。这些都是非常有价值的。
比如说我过去一个月内,花了不少的精力,将相关 Service 的文档补全,是个很典型的例子。这一些活,对于后续进入社区的成员来说,价值非常的大,但是做起来很苦逼,所以一直搁置到现在。这些活都会是一个良好的切入点。那么可能有人要问一个问题了,你去做这些脏活累活值吗?这个问题就需要去从你参与开源社区的出发点去讨论了,是去获取 reputation,然后升职加薪?还是 just for fun?如果是前者,那肯定不值,这位同学你肯定也不想在你的晋升答辩上协商善于做脏活累活吧(逃。如果是后者,我个人觉得很值,而且我自己也乐在其中,因为实际上在梳理整个文档,做很多脏活累活的过程中,实际上是 push 我去完整的梳理 OpenDAL 相关的 Service 和一些背后的逻辑。这个过程无论是对于我技术栈的提升还是对于项目本身的理解的提升都是蛮大的
说到另外一个问题,就是很多人的畏难情绪,我不只一次将一些合适的 Issue 放到不同群里,但是得到的回复通常是“我不会xx,该怎么办?”。学啊?而且很多时候熟不熟练真的没那么重要的,我记得在实现 OpenDAL 里面一些 Service 过程中,发现了一个优化点,feat: change blocking_x in async_x call to tokio::task::blocking_spawn,在我发到一些群里后,一位正在学习 Rust 的群友接下来这个任务,而且完成的很好。实际上去做自己最熟悉的东西是不太会给你打来成长的,去跳出舒适区,去和不同人交流,去做一些自己不熟悉的事情,才是最好的成长方式。
最后还有一点非常的重要,你自己经历的珍贵,你自己可能都没意识到。每个人不同的职业,学习经历会塑造一个人不同的看问题的视角,而不同的视角是一个社区最珍贵的财富。其实这个月里,我对于 OpenDAL 可观测性相关的 Layer 的工作(包括完善 Metric,目前正在实现中的引入进程调试工具等工作)实际上就是来自于我作为一个 SRE 对于可观测性基本本能。而我这样一些 SRE 的视角,也给 OpenDAL 带来了整个稳定性相关功能的迭代与进步。
其实说了这么多,千言万语汇聚成一句话,如果你想去参与开源社区,那么就去勇敢参与,不需要给自己设限。你一定会在你不断的参与的过程中得到许许多多的收获,认识不同的人与事。
开源真没你想的那么难
]]>在聊今天的正式内容之前我们需要理解 Python 在内存中的布局
对于传统的 native application 而言,大家对于其内存布局应该是比较熟悉的,这里以 x86-64 的一张图来说明其栈帧结构
但是对于 CPython 来说,其 Native Code 执行的只是 VM 一层的代码。其在 VM 内单独抽象了一套类似 native 的栈帧结构。
其核心结构如下
1 | struct _frame { |
其在内存的组织结构大概如下所示
这里不难理解,每个栈帧中都包含了当前栈帧的上一个栈帧的指针,这样就形成了一个完整的栈结构。
同时我们能看到,在 Python 的栈帧结构中包含了很多重要的信息,
我们所有的 trace/回溯的操作,都需要来基于这些信息来进行。
OK,在大致了解了 Python 的栈帧的一些入门知识后,我们接着往下聊
我们通常对于在外部调试 Python 的时候,无外乎有两种需求
在 Python 3.12 之前,社区已经对于这样一些内容有了尝试
Python 目前对于 Trace 的尝试最早可以追溯到2014年,在 3.6 发布前夕,Python 提供了 DTrace 的支持,参见 Systemtap and DTrace support1
DTrace 是在 Unix/Linux 下提供的一种用户预置一些埋点的基础设施。在对应的函数预置埋点后,对应位置的调用可以触发外部的注册程序(包含 SystemTap/eBPF 等),从而实现对于一些调用时的动态 trace。
Python 提供了一部分预置的 Hook 点
1 | 29564 python18035 python3.6 _PyEval_EvalFrameDefault function-entry |
然后下面是一个使用 systemtap 的例子
1 | probe process("python").mark("function__entry") { |
效果差不多这样
1 | 11408 python(8274): => __contains__ in Lib/_abcoll.py:362 |
但是目前来说,通过 DTrace 暴露的信息还比较少,在一些复杂的场景(比如多线程,协程),对于一个函数多次调用后,我们很难去将具体的函数关联到具体的调用栈中。
这个时候就需要涉及到去对于 Python 栈帧结构的处理了。虽然 USDT 没有将 FrameObject 指针传入到我们的注册 hook 程序中,不过要获取的话,实际上也不算太难
首先我们利用 readelf 看一下 Python 二进制中 .note.stapsdt
section 中的内容,
1 | Displaying notes found in: .note.stapsdt |
我们能看到 function__entry 中起始的地址是 0x00000000002693bf
,然后我们可以在 gdb 中将对应的部分反汇编出来
1 | 0x5555557bd3ef <dtrace_function_return+31>: call 0x55555575be00 <PyUnicode_AsUTF8> |
其中 NOP 指令是一个 trick,在外部有程序 attach 到进程上后,NOP 会被替换成 INT3 进入调试模式。
然后我们看到上一个调用是 call _PyInterpreterFrame_GetLine ,而这个函数的参数的原型是
1 | int _PyInterpreterFrame_GetLine(_PyInterpreterFrame *frame); |
OK 基于 X86 的调用约定,rbi 用于存放函数第一个参数,即我们需要获取的 frame 对象,那么这里实际上在我们的 probe 程序中获取 rbx 寄存器的值就可以获取我们需要的 frame 对象了。
这样就能基于 DTrace 来完成一些 trace 的需求了。
但是目前基于 DTrace 的方案有这样一些问题
其实这一部分可以了聊的东西相对没那么多,主流的工具,如同 py-spy 这样的都是通过 process_vm_readv 这样的奇怪的 syscall 来处理的。本质上还是对于 FrameObject 进行各种解析,缺陷差不多有这样一些
我们都知道,对于非 NATIVE CODE 来说,perf 很多时候没有办法生效,因为你找不到对应的地址和具体的符号之间的映射,也无从谈起去采样。
好在 2009 年 Linux 3.x 之后,perf 提供新的功能,它允许用户往 /tmp/perf-%d.map
文件中写入地址与符号之间的映射,这样 perf 就可以通过这个文件来解析地址了。具体可以参见 perf report: Add support for profiling JIT generated code share3
在 Java/Node.js 都对于这个功能有了支持后,Python 也在 3.12 中提供了对于这个功能的支持,具体可以参见 gh-96143: Allow Linux perf profiler to see Python calls4
这个功能的实现其实是一种局部的 JIT,我们来看看具体的实现
1 | typedef PyObject *(*py_evaluator)(PyThreadState *, _PyInterpreterFrame *, |
其实这里的思路很巧妙,有几个核心的点
那么怎么编译 trampline 呢?我们来看看具体的实现
1 | static int |
我们看到其实编译的操作本质上是从内存区域内新申请一块区域,然后将对应的 trampoline 拷贝到这个区域中,然后将这个区域设置为可执行的,这样就可以了。
而对于 trampoline 的实现,其实就是一个汇编的实现,我们来看看具体的实现
1 | .text |
我们看到这里的汇编其实就是一个简单的 call 操作,然后将返回值返回即可。等价于
1 | PyObject * |
那么我们其实这里的思路就不难理解了,我们在编译 trampoline 的时候,会将对应的 FrameObject 传入到 trampoline 中,然后我们在 perf map 文件写入的时候,实际上是将符号与内存中的一块固定区域进行了 binding。这样让我们的 perf 就可以通过 perf map 文件来解析对应的符号了。
那么我们来看看具体的 perf map 文件的内容
1 | 7f0caf8aa70c b py::_path_abspath:<frozen importlib._bootstrap_external> |
这样的好处有很多,我们可以利用 Linux 本身的 perf 生态,来完成很多基本的工作(比如火焰图)
同时我们在去做一些具体的函数的 trace 的时候,我们也可以利用 uprobe 之类的工具,基于我们在 perf 中写入的映射,来做更进一步的 trace,这也让整个 Python 程序的 trace 变的更容易,不用考虑汇编,不用考虑平台(其实也要考虑的(目前只有 ARM/X86 支持))
Python 3.12 这个新特性真的是又新又好,实现的非常巧妙。其实之前和人讨论到 WASM 的 perf 的支持的时候,我第一反应也是可以参考 Python 中类似的做法,不过 WASM 先把自己的 WASI 搞成熟吧,不然花式 host function 也没啥 perf/trace 的必要了。
差不多就这样
写这篇文章的原因是之前给 runc 提的 CPU Burst 支持的 PR [Carry #3205] libct/cg: add CFS bandwidth burst for CPU 终于开始有了新的动静了,这次换了一个国人的 reviewer,感觉要是运气好能在9月开始合并这个 PR。
如果这个 PR 被合并了,那么在 containerd/nerdctl 等其余项目上支持 CPU Burst 的工作就可以开始了。所以这篇文章就是想记录下我对于 CPU Burst 在 Kubernetes 内实现的一些想法,差不多可以当作自己写正式的 KEP(Kubernetes Enhancement Proposal) 草稿
主要分为两个部分来聊一下
聊 CPU Burst 之前必须要先聊一下 Linux 里面关于 CGroup 的一些背景知识
提到 CPU 限制,本质上是限制进程的 CPU 使用的时间片,在 Linux 下,进程存在三种调度优先级
1 用的是 Linux 中 CFS 调度器,而常见普通进程都是 SCHED_NORMAL 。OK 前提知识带过
说回容器中的 CPU 限制,目前主流语境下,容器特指以基于 CGroup 的容器方案为代表的一系列的基于 Linux 中 CGroup 和 Namespace 进行隔离的技术方案。那么在这个语境下,CPU 限制的实现利用了Linux CGroup 中三个 CPU Subsystem。我们主要关心的如下四个参数
现在分别来聊一下
首先说 cpu.shares,在基于 CGroup 的容器方案中的使用参数是 —cpu-shares,本质上是一个下限的软限制,用来设定 CPU 的利用率权重。默认值是 1024。这里对于相对值可能理解有点抽象。那么我们来看个例子 假如一个 1core 的主机运行 3 个 container,其中一个 cpu-shares 设置为 1024,而其它 cpu-shares 被设置成 512。当 3 个容器中的进程尝试使用 100% CPU 的时候(因为 cpu.shares 针对的是下限,只有使用 100% CPU 很重要,此时才可以体现设置值),则设置 1024 的容器会占用 50% 的 CPU 时间。那再举个例子,之前这个场景,其余的两个容器如果都没有太多任务,那么空余出来的 CPU 时间,是可以继续被第一个 1024 的容器继续使用的
接下来聊一下 cpu.cfs_quota_us 和 cpu.cfs_period_us ,这两个是需要组合使用才能生效,本质上含义是在 cpu.cfs_period_us 的单位时间内,进程最多可以利用 cpu.cfs_quota_us (单位都是 us),如果 quota 耗尽,那么进程会被内核 throttle 。在基于 CGroup 的容器方案下,你可以利用 —cpu-period 和 —cpu-quota 这两个值分别进行设置。也可以通过 —cpu 来进行设置,当我们设置 —cpu 为 2 的时候,容器会保证 cpu.cfs_quota_us 两倍于 cpu.cfs_period_us,剩下的就以此类推了(Docker 默认的 cpu.cfs_period_us 的阈值是 100ms 即 10000us)
在这种模式下,CPU 的时间片按照时间维度基于 period 进行切分,那么在我们实际的生产应用中,我们将会遇到这样的情况,突然来了一波流量/一个任务,进程消耗完了所有的 quota 后,那么将会进入 throttle 的状态。这会导致我们整个响应的 P99 出现很大的毛刺。
CPU Burst 这个特性就是为了解决这个问题而生,它的原理是在已有的语义基础上,新增一个参数 cpu.cfs_burst_us (在 CGroup V2 中 cpu.max.burst),即进程可以在 CPU 利用率比较低的空闲时段积累一定的 credit,然后在密集使用的时候换取一定的 buffer,实现更少的 throttle 和更高的 CPU 利用率(当然这个特性还暂时没有被主流容器所完全支持)
这里可能有人会问,这样不会导致 CPU 限制失效吗?虽然本文不会讨论 burst 的实现(可以单开一篇文章聊),但是可以先给一个结论,目前来看,暂时从数学的角度上利用 WECT(Worst-case Execution Time) 没法给出一个证明说 CPU Burst 是完全可靠的,但是根据已有的测试结果来看,在 CGroup 数量比较多 & CPU 利用率整体不高的情况下,边界是收敛的,具体可以参见相关的讨论1
OK,关于 CPU Burst 的背景先聊到这里
聊完 CPU Burst 的背景,我们需要来聊一下 Kubernetes 对于 CPU 资源怎么做的分割
首先我们起手一段祖传 YAML
1 | resources: |
抛去内存的部分,我们先讨论 CPU 的部分,这个地方很容易理解,我这个 Pod 需要百分之5核的 CPU,最多允许使用10%核的 CPU。
然后在这里,m 是指千分之一核,也就是说 1000m = 1核,那么这里的 50m 就是 5% 核,100m 就是 10% 核。OK 接着往下聊
首先对于我们 requests 部分,Kubernetes 在调度的时候,会利用我们之前提到的 cpu.shares 来设置
而对于我们 limits 的部分,Kubernetes 在调度的时候,会利用我们之前提到的 cpu.cfs_quota_us 和 cpu.cfs_period_us 来设置,在 Kubernetes 中,cpu.cfs_period_us 的默认值是 100ms 即 10000us,那么在我们的例子中,cpu.cfs_quota_us 的值就是 100ms * 10% = 10ms。
OK,大家可能都比较熟悉 Kubernetes 里面的资源的一些基础概念了。那么我们接着回到本文的正题部分:如何在 Kubernetes 中实现 CPU Burst
在 Kubernetes 中实现 CPU Burst 核心的两个问题
这个地方我会考虑三种方案
在聊这三种方案之前,社区其实已经有了一些实现,也是阿里做的,我们先来看一下阿里的实现
首先我们来看一下阿里的实现,这个实现是在 koordinator 中实现的,具体的实现可以参见2
他们通过 CRD/configmap/annotation 多种方式都可以实现对于 Burst 的支持,比如看一个例子
1 | apiVersion: v1 |
那么问题来了,现在 Kubernetes 中还不支持 burst,那么怎么做的呢,我们直接看下他们核心代码
1 |
|
这段逻辑其实很简单,他们的做法很暴力,直接将 koordinator 跑了 daemon,直接更新了 CGroup 的值,然后通过 CRD/configmap/annotation 来控制是否开启 CPU Burst。同时对于低版本的内核,通过调整 period 和 quota 来实现类似 burst 的效果。。
不得不说,的确是实现了目标,但是缺点也很明显,实际上是破坏了 Kubernetes 对于资源的调度。路子很野。
那么我们接下来来聊聊我们的几种实现的思路
首先大家肯定清楚,在 Kubelet 中存在两种 CPU Manager Policy
后者对于 Guaranteed 类型 QoS 的 Pod 可以进行绑核操作,我们可以通过在 Node 打上 Label ,然后利用 NodeSelector 和亲和反亲和之类的工具来完成 Pod 的调度
那么同理,我们可以类似在 Kubernetes 中新增一个 CPU Manager Policy 的策略,叫作 Burst,然后新增一个配置字段 BurstQuotaPercentage。这个字段决定了我们为 Burstable 类型的 Pod,新增 BurstQuotaPercentage * cpu.cfs_quota_us 的 cpu.cfs_burst_us 的时间。
这样的好处有这样几个
但是缺点也很明显
这个方案核心在于调整 Resources 的语义,使其可以使用这样的方式来实现 CPU Burst
1 | resources: |
这个方案的好处有这样几个
但是缺点也很明显
实际上类似于阿里已有的方案,我们已知 Kubernetes 存在一个 PodDistruptionBudget 的 CRD
1 | apiVersion: policy/v1 |
那么我们实际上也可以设计一个类似的 CPUBurstConfig 的 CRD 来实现
1 | apiVersion: policy/v1 |
同时我们也支持通过 annotation 来进行配置(因为 namespace crd 最终会转化为 Pod 上的 annotation)
那么这样的方案很明显
但是缺点很明显,和 resources 方案差不多,会带来一个抽象泄漏的问题
差不多就这些吧
这篇水文差不多就到这里了,算是对于我自己想做的一个 KEP 的一些设计思考吧。不过有一说一,各种 tradeoff 实在太难做了。属实麻了。差不多就这样吧。改天有空再水点其余的文章吧。
昨晚三点过刚吃完药躺在床上休息的时候,突然想到了 @yihong0618 的之前在群里的一个想法
我在想 eBPF 能不能 trace libpq 的协议,好像还没有人做过
我最开始的一个想法是
现在主流做法还是 ptrace 系的东西(gdb 那套),你要用 eBPF 去 trace libpq 肯定没问题,就和 Grey 用 uprobe 去 trace go 一样,手算 cast。但是这里另外一个问题是 libpq 的符号信息不一定够。我倾向你可以这样试一下,你改一下 libpq 源码,关键地方走 USDT(我看你之前用过)
不过后续我师父出来有了一个提醒
如果目标是 trace libpq.so 的调用情况,那应该目前就可以做到。
.so 相比 executable 有几个优势:
- 它一定有动态符号表
- 它一定有 .eh_frame
uprobe 恰好又是 attach to the binary offset 而不是 process address,所以第一个优势完美匹配 uprobe,甚至绕开了 executable 本身如果是 PIE 的复杂情况。
栈回溯则完美利用了第二个优势。举例来说,默认的 libc.so 里的函数都是 -fomit-frame-pointer 所以不能用 bp = *bp 来回溯,但是可以用 FDE (Frame Description Entry) 来回溯。DWARF 的 .debug_frame 和 .so 的 .eh_frame 就包含了这样的信息,所以足够让我们从 .so 回溯回 executable。
所以目前的基建已经完全足够做一个 libpq.so 的 bpf tracer,而不需要任何前提假设。
的确是。。LSB 规定的信息足够多,我之前忽略了这点。一般发行版都是带了 .eh_frame, 里面的 CFI 是可以做栈回溯的,而且我看了下 PG 默认的编译是没开 no-asynchronous-unwind-tables 的,基本上可以确保一定会带这 eh_frame。(注:这个选项用来控制是否生成 .eh_frame)
所以说做就做,爬起来想先做一个 PoC。先去 Review 了一下手上的工具,发现可能 systemtap 是比较适合的工具,他内部已经实现了一套 DWARF,符号表相关信息解析的功能,所以可以直接复用,先写了一个 Demo 的 PG 代码
1 |
|
然后起起 systemtap 写个钩子
1 | probe process("/lib64/libpq.so").function("PQconnectdb") { |
执行效果是这样
1 | probe process("/lib64/libpq.so").function("PQconnectdb") { |
没什么问题,心满意足的准备去睡觉
起来后顺便复习了一些用户态栈回溯的细节
用户态栈回溯核心的一个要解决的点就是增强程序的可追踪性。
在传统 X86 模式下,我们有这样的栈帧结构
大体概括就是利用 ebp 寄存器保存栈帧地址,esp 保存栈顶指针,在调用过程里,将上一个栈帧地址入栈保存
在这种情况下,我们不管是调试器还是其他的工具,都可以通过 ebp 和 esp 来进行栈回溯,这种方式优缺点都很明显
优点就是足够的简单,缺点的话大概有这样一些方面
所以在这种情况下,进入64位时代后,这种栈帧结构被放弃,gcc 在64位编译下默认不使用 rbp 寄存器来保存栈帧地址了(不过可以通过 -fno-omit-frame-pointer 选项打开)
在栈帧结构变化后,我们现在要进行栈回溯,就需要依赖额外的一些调试信息了。
说到调试信息,大家第一反应肯定是 DWARF (aka Debugging With Attributed Record Formats),在这一套信息中,定义了一套 CFI (Call Frame Information) 的规范,用来描述栈帧的结构,这套规范在 GCC 和 LLVM 中都有实现。目前 CFI 相关信息存放在程序的 .debug_frame 和 ,.eh_frame 段中。我们可以用一下 readelf 来查看,这里以我们上面的 C 代码的编译结果为例
1 | 000000b4 000000000000001c 000000b8 FDE cie=00000000 pc=00000000000012fc..0000000000001311 |
这里的 FDE (Frame Description Entry) 就是一条 CFI 信息,里面包含了一些寄存器的信息,比如 rsp, rbp, ra 等等,这些信息可以用来进行栈回溯。
整个过程差不多如下
当然这里还有很多工程的部分要去做,比如你需要走 auxv 去拿到进程加载后的 ELF,你需要去遍历符号表之类的东西(XD
现在有一些成套的基础设施可以用,列一下仅供参考
实际上这个工作还有很多的内容要去做,比如你通过 FFI 调用 so 后,你直接进行 native 的栈回溯得到的结果是这样
1 | 0x7f641a21aea0 : PQsendQuery+0x0/0x10 [/usr/lib/libpq.so.5.15] |
如果你想拿到 FFI 另外一侧的信息,那又是一翻额外的工作量(比如 Python)
以及 unwind 下还有很多噩梦级别的 case 要去处理,比如 PIE,比如 strip 信息后的二进制。如果想用 eBPF 重写 libunwind 的话,我觉得跳楼可能更快一些(不是
所以遇到问题的时候,可能优先考虑编译一些带着埋点的二进制文件。有可能你搞 print 大法都比 unwind 更好用(XD
差不多这样
最后,推荐一个雄文
]]>这个问题实际上是来源于在群里和人的一个讨论。一个基本常识是,子进程退出后,父进程会收到 SIGCHLD
信号,然后父进程可以通过 wait
或者 waitpid
等系统调用来获取子进程的退出状态。那么,子进程退出后,父进程有可能会收不到信号吗?答案毫无疑问是 yes 的
本文就来聊个其中一个比较好理解的场景 BTW 本文代码都基于最新分支的 Linux 源码
Fuck,哦不,Shut up,我们先来看一段代码
1 | import os |
小学生级别的代码,那么这段代码我们预期是什么?很简单嘛对嘛,最后 result 和 count 相等。那么我们来看一下这段代码的执行结果
1 | root@kernel-dev-1:~/demo-script# python3 fork-demo.py |
emmmm????,然后我们发现机器上也出现了 Z 进程
为啥捏?这一切都是为啥捏
要聊清楚这个问题,就得从两方面入手
那就继续聊
进程退出后,内核核心的一个函数调用是 do_exit, 位于 /kernel/exit.c
,看一下代码
1 | void __noreturn do_exit(long code) |
这段代码其实看着很轻松,基本上看调用函数名就能知道在干啥,比如 exit_fs
卸载文件系统啊,exit_files
清理关联文件啊,exit_notify
进行 reap 之类的操作啊,然后我们能一眼到一个函数 exit_signals
,这个函数是干啥的呢?看一下代码
1 | void exit_signals(struct task_struct *tsk) |
其实前面都是一些准备操作,比如加锁准备操作,cgroup 的前置操作啊,在这些完成后,do_notify_parent_cldstop
将是我们最终执行信号发送的地方,看一下代码
1 | static void do_notify_parent_cldstop(struct task_struct *tsk, |
这个函数稍晚有点长,我们依次来解析下
1 | if (for_ptracer) { |
通过 task_struct 的定义,查找父进程,准备下面的操作
1 | clear_siginfo(&info); |
设置 info 中的信号为 SIGCHLD
1 | if (sighand->action[SIGCHLD-1].sa.sa_handler != SIG_IGN && |
发送信号,然后唤醒父进程,最后释放锁。
现在我们差不多搞清楚了在进程退出时,内核怎么发信号的。
但是一个问题还是没解决,为什么在我们的 case 里有信号没拿到?
那么接着聊
花开两朵,各表一支,我们上文看到,最后我们在进程回收的时候,调用 send_signal_locked
发送信号。在继续聊这个问题之前,我们需要来了解一些简单的 Linux 信号的知识
在 Linux 中,Linux 将进程分为了 real-time 和 standard 信号。后者通常又有一个别名叫作不可靠信号。通常来讲,信号值小于 SIGTMIN 的为不可靠信号,信号值大于 SIGTMIN 的为 RT 信号。Linux 对于 RT 信号的特性有如下描述
- Multiple instances of real-time signals can be queued. By contrast, if multiple instances of a standard signal are delivered while that signal is currently blocked, then only one instance is queued.
- If the signal is sent using sigqueue(3), an accompanying value (either an integer or a pointer) can be sent with the signal. If the receiving process establishes a handler for this signal using the SA_SIGINFO flag to sigaction(2), then it can obtain this data via the si_value field of the siginfo_t structure passed as the second argument to the handler. Furthermore, the si_pid and si_uid fields of this structure can be used to obtain the PID and real user ID of the process sending the signal.
- Real-time signals are delivered in a guaranteed order. Multiple real-time signals of the same type are delivered in the order they were sent. If different real-time signals are sent to a process, they are delivered starting with the lowest-numbered signal. (I.e., low-numbered signals have highest priority.) By contrast, if multiple standard signals are pending for a process, the order in which they are delivered is unspecified.
简单来说,RT 信号是可靠且有序的,在内核中,task 的解构包含了两个关键结构体
1 | struct sigpending { |
很明显,这两个结构体是内存中的链表,sigpending 中的 sigset_t signal 是个 64位整数,每个结构体占据一位,标注是否有信号触发。
我们来看下=send_signal_locked
的代码
1 | int send_signal_locked(int sig, struct kernel_siginfo *info, |
前面又是一堆准备操作,我们直接把目光转向 __send_signal_locked
函数
1 | static int __send_signal_locked(int sig, struct kernel_siginfo *info, |
这里我们核心关注这样一些地方
1 | if (legacy_queue(pending, sig)) |
这里是判断当前发送的信号,是否已经在 sigpending 中的 sigset_t signal 中注册,如果注册了,就直接进入返回流程
1 | static inline bool legacy_queue(struct sigpending *signals, int sig) |
嗯这段逻辑就很清晰了,继续回到 __send_signal_locked
函数
1 | if (sig < SIGRTMIN) |
这里判断是否需要强制超出系统 SIGNAL QUEUE 的长度限制,然后调用 __sigqueue_alloc
函数,这个函数的作用是分配一个 sigqueue 结构体,然后将其加入到 sigpending 的链表中
1 | static struct sigqueue * |
这里的逻辑就很清晰了,分配内存,返回指针。这里系统中 RLIMIT_SIGPENDING 的配置决定了我们 pending 队列的长度。各个发行版不同
差不多这样
回到我们最开始的代码
1 | import os |
这段代码,在我们的回调函数中存在 block 行为,导致后续进程在 SIGCHLD 信号发送后,在 legacy_queue
处判断当前队列有同样的非可靠信号未被处理完,于是没有完成后续的信号处理流程
嗯,差不多就这样。简单写篇入门水文,希望大家看的开心
]]>首先,今天毫无疑问是个 big day,接到了一个超好的消息。我资助的来自大凉山的一位女生,在父母都大部分失去劳动力,国家级贫困县的极度贫困家庭的情况下,在高考中拿下了550的成绩。超出一本线30分,算上凉山地区的加分,她应该可以去一个合格的公立学校接受完整的高等教育了。
当时接到消息的时候,我简直开心炸裂,我和女朋友都兴奋的跳了起来。原因很简单。她这一路走来太过不容易。
我是从她高一下学期开始资助她的,那时她的成绩仅仅排在年级中游,本科很悬。在后面的只言片语以及机构每学期的回访中。我都能看到她的迷茫,与坚持。她一步步的,一点点的努力着。然后在今天,终于圆梦。虽然这个梦对于很多网友来说微不足道。
我在推特和朋友圈分享这对我来说也是意义重大的喜讯后。迎来了很多的懂王。发表了一些典型的爹味和傻逼言论,“她研究生和博士你也帮吗”,“过去的贫困让她有心理疾病,没治愈之前没法说改变人生”,“出来还是当韭菜的命”
分开聊聊吧
“她研究生和博士你也帮吗”,对啊,我帮。咋了?
“过去的贫困让她有心理疾病,没治愈之前没法说改变人生”,她比你想的坚强,也比你想的阳光。三年里,我见证了她的蜕变于进步。她从未想过放弃自己,一步步的走了出来。我从未质疑过她面对未来的勇气,我相信包括我在内的所有人亦没有资格去质疑她面对未来的勇气与坚强
“出来还是当韭菜的命”,我自己也算是大凉山出来的(攀枝花),小时候家对面山上都能看到吸毒死亡的人,更别说凉山了,吸毒贩毒到处都是。上不了高中是常态中的常态。我资助的女孩子,父母都失去劳动能力,家里极度贫困。但是一步步的上了当地最好的高中,又一步步的超出一本线这么多(算上加分,超一本线能40),她的命运已经改变了。今后的她即便在城市里996,当着你们高等人所谓的韭菜。过着你们高等人所看不上,所觉得淡然无味的生活。但是这种生活远比她之前过的生活要强出千百倍,她不用担心家门口是不是有吸毒的人,也不用担心自己是不是要十五六嫁人换彩礼。然后如果她愿意,找个丈夫,结婚,生子,相守平淡一生。你们所看不上的生活,是她梦寐以求的生活。
说实话,公益和助学的意义永远不在于说一步到位,让人飞黄腾达。而是一点点的,让更多经历苦难的人,能够脱离环境桎梏。
我从大学时期进入救援队做公益,到现在也有11年了。后续历经2013雅安,2017年北京等大型事件。而且捐款助学的话,从2017年开始进行捐助。到现在也快7年了。说实话,很难去具体的描述我做公益的动机。不过强行解释的话,那么其实大概能概括成这几点
能从上面看出来,其实我没有太强的目的性去做这件事。实际上这也是我所推荐的状态。我个人觉得如果具有太强的功利心是做不好这件事的。
而从16年后,我整体的公益主要以线上为主了(因为我实在没时间和体能跑现场了,除了17年北京还跑了一次现场),剩下的主要有以下几个方面了
这里我想先说说4。实际上这是一件非常非常非常重要的事。很多同学可能会发现,我自己很喜欢在推上和朋友圈里分享自己的公益日常。开心的,不开心的。我估计有不少人实际上会认为这什么鸟人,这么喜欢秀。实际上不然,我是想通过分享我做公益的细节与瞬间,能让更多的人去发现公益的乐趣。然后投入到这件事。实际上效果不错。从18年到现在,我至少号召了四位身边的朋友助学捐款,还有一位成为了公益组织的义工。是不是很酷!
然后回到公益资助本身。2的话,我实际上没有很挑选过公益机构。在腾讯公益或者支付宝上捐个三五百的就完事(当然累计也有一定数额了)。我自己投入精力比较多的部分,也是大家比较关注的部分还是在3,即助学捐款上。我认为这是一件非常意义的事,如本篇博客 cover 中所记录的。教育是最好的公益
通常来说,目前我觉得比较靠谱的渠道有两个,一个是鲲鹏公益计划,一个是壹个村小,前者在支付宝公益和微信公益上都能找到,我目前捐款了几次,差不多一次2000以内。这个项目是典型的一对一项目。定期会有学生情况更新。所以相对来说更有保障。而我自己一直很喜欢的团队是壹个村小,他们的微信公众号是 cunxiao4u。这个团队我觉得很特殊,主要在以下几点
每个学生的资助金额以学期计算,差不多1000-3000不等。具体金额是根据学生具体情况计算出来,每学期都会更新与你同步
那么他们有没有缺点?有,非常大的缺点,超级大的缺点,我怨念深重的缺点!就是他们学生太太太太太难抢了。真的。。说起来就是一把泪。。每次抢学生都需要八点过起来。然后到点一刷秒没。属实是人麻了。
这些差不多是我自己关于助学公益的一些建议。那么回到做公益这件事本身,可能会有人问你有没有自我怀疑的时候,那毫无疑问,有啊,非常有,常见不仅限于
这些我都有过,但是我后面想通了,可能说看淡了
继续聊之前,我先给大家科普一个知识,首先我们定义超长心肺复苏是指时间大于半小时的心肺复苏。即便在现代高度发达的医学下,根据现有的数据,一般在两百例超长心肺复苏才能遇见一位能痊愈出院的患者。这个比例很低对吧?但是大家换个角度想,如果全球每年有2000万例超长心肺复苏,那么意味着会有10w名患者获得新生。数字又不小了对吧。如果说,我们放弃了这2000万次超长心肺复苏,那么意味着有10w条本应该能救回来的生命再次逝去。
公益和心肺复苏一样
有很多钱没法用到正途,没错
有很多人骗取同情心,没错
很多重男轻女家庭用姐姐出来博同情心,给弟弟赚钱,太他妈对了
各种公益组织贪污情况太严重了,fucking right
有太多的人需要帮,我们帮不过来,damn right
但是这些都不是我们不去做,和犹豫的理由
我一直坚信只要我们做的事足够多,那么就一定有足够多的人获取帮助
水坑里的鱼太多?我救不完,fucking right,but so what?这绝不是我一条都不救的理由
所以我希望我们还是能一点点的尽力帮助所需要帮助的人。这个世界足够操蛋,我们也没有办法改变这个世界,但是我们可以让这个世界多那么一丝色彩
最后一起复习一下艾特奥特曼的愿望
優しさを失わないでくれ。弱い者を労り互いに助け合い。
どこの国の人とも友達になろうとする気持ちを失わないでくれ。
たとえその気持ちが何百回裏切られようとも。
それが私の最後の願いだ。
热忱之心不可泯灭。要体恤、帮助弱者。
与任何国家的人都能成为朋友,别失去这份热心,纵使它已被背叛了千百回。
这就是我最后的愿望。
To love, to lose, to give without expectation
Yeah, 其实公益很简单的,不需要有那么多顾虑,也不需要有那么多成本,我们随手捐一点,帮一点,我们所有人的力量就能够一定帮到足够多的人。就如同超长 CPR 里那10w条被拯救的生命
这个世界越来越操蛋,也越来越混沌了。这个时代是否如狄更斯所说“这是一个最好的时代,也是一个最坏的时代”,我对前半句抱有怀疑。
但是我依旧愿意去相信爱,光和奥特曼,人生苦短,总得做点什么,让这个 fucking ridiculous 的世界变得有那么彩色一点。
Love and Hope is all we need.
]]>今年对于我来说其实是挺 tough 的一年,双相的病情反反复复。自残的行为也重新出现。特别是在今年3月之后,我某种意义上的心理防线一度被击穿。
熟悉我的朋友可能已经反应过来了。没错,3月24日,杰克奥特曼人间体乡秀树扮演者团时郎先生的离世,实际上给了我不小的打击。
我依旧还记得那天的情景。在一个朋友给我转发了日媒报道乡秀树先生3月22日离世的消息后,当时嘣的一声,我脑子里的弦仿佛断了一根。眼泪就不由自主的流了下来。我甚至一度都没有反应过来我在流泪这件事。直到我妹子发现了我的异常问我怎么了。我才反应过来,原来我在流泪。当天实际上我一度无法正常工作了(即便现在,我回想起来也有点泪目),后面算是勉强打起精神,给当天的工作收尾。
实际上这件事对我的影响贯穿到现在,导致我4月/5月情绪非常的波动。我曾经给我设置了几道心理的防线,其中一道是和奥特曼相关的,然后这道防线在当时被击穿了。
实际上团时郎先生的逝世应该是个导火索。早在去年5月份,我最喜欢的奥系列中的一位演员渡边裕之先生(盖亚奥特曼中石室指挥官,我认为特摄史上塑造的最成功的人物之一)在公寓里自杀的消息实际上也深深的刺痛了我。只是当时的我没想到,事隔一年后,我还会迎来另外一位人物的离世。
很多人可能会很疑惑,为什么我会如此喜欢奥特曼。
这里我要更正一点,我不是喜欢奥特曼,而是“奥特曼是我的信仰”,或者说,我信仰着奥特精神。
这样一个信仰的形成,其实要追溯到我的过往。
我之前在博客上分享过,我在2007年初遭到了同性的强奸。虽然在后续父母对我保护的很好。但是因为事件之初没有进行相应的心理干涉。实际上这件事的诱发的 PTSD 一直持续到了现在。而在初高中,即我人生13-18岁这五年三观的成型期,强奸事件带来的残缺感与孤独感一直伴随着我(我其实是个很怕社交的人,没想到吧)。同时,这样一份残缺感+我并不愿意和父母做太多的沟通,导致我这个时期的阴暗的一面非常严重。你可能能想到的暴戾,自残,嗜血(比如时常扣掉伤疤舔血)的一面都在我身上存在。可能和新闻上人物的区别在于当时的我没去实施。如果这样发展下去,可能现在你们就会在新闻上看到我了。
所幸,这个世界上真的存在奥特曼
在这五年的时间里,我时常在苦闷,烦恼,暴戾情绪严重的时候,逃课或请假,找个奶茶店或者网吧(是的,我去网吧不打游戏),点点吃得,然后坐着看一天奥特曼。
这样一天天过去吗,我心里曾经空掉的东西,被一点点填了回来。某种意义上来说,奥特曼,或者说奥特精神是构成了我这个人现在所表现出来的一切正面元素的基石。
可能有很多人会问,奥特精神,到底是什么?这里我引用来自艾斯奥特曼最终话的台词,应该就能做出很好的解释
優しさを失わないでくれ。弱い者を労り互いに助け合い。
どこの国の人とも友達になろうとする気持ちを失わないでくれ。
たとえその気持ちが何百回裏切られようとも。
それが私の最後の願いだ。
热忱之心不可泯灭。要体恤、帮助弱者。
与任何国家的人都能成为朋友,别失去这份热心,纵使它已被背叛了千百回。
这就是我最后的愿望。
贯穿奥特曼全系列的,就是这样一份热忱之心,一份坚守的精神,一份不屈不挠的意志。这份精神,也是我一直以来所追求的。
这里我推荐大家如果有兴趣可以去看一下下面两作
我觉得如果大家能看完,实际上会对奥系列一直传达的希望,反战,和平,理解,爱有非常深刻的感触。
13-18岁,这人生很关键的五年,奥特曼陪伴了我。基本上塑造了我现在的很大一部分人格和认知(我现在依旧坚信着平行世界一定存在的奥特曼的Hhhhh)。也会实际上影响我做事的方式。想做什么坏事的时候,心里想想,这可能不奥特精神。在帮不帮人犹犹豫豫的时候,我想如果做了那么奥特曼一定会替我开心的。
我一个好友这样评论我的想法(大家别说我幼稚啊):很多人只是会把你这些想法抽象为佛主,或者上帝。而你只是抽象成奥特曼而已。
Exactly!
之前有推特上有懂哥跳出来评价
没错,我就喜欢了,我就傻逼了,怎么了?
随便碎碎念了一下,算是让心里一些一直想说的话说了出来(好受了许多)
谨以此文,献给团时郎先生,献给渡边裕之先生,献给我所深爱并信仰着的奥特曼。
如果有人能认识历年的主演们,请替我给他们转达一句话:谢谢你们,谢谢你们塑造的奥特曼拯救了我
最后的最后:ウルトラマン大好きだ
]]>熟悉我的朋友都知道,我是个 SRE,所以吧,对于家里的网络环境质量要求非常高。一直以来,我家的网络环境都在不断的迭代,不过我的网络环境一直围绕三个方向迭代
那么围绕这个,从今年开始,我围绕 Homelab 做了一系列的演进
在今年4月搬到新家后,借着要重新安排所有东西的机会,我重新构造了家里的网络
首先来讲,从全屋的无线热点上,我做了这样的演进
在经过这样的改造后,我家里的网络结构变成了这样的模式
另外,今年六月,好友赠送了我一个 Intel 12代 i7 的满配 NUC ,我自己也采购了两台零刻的机器做为家里的 NUC 集群,上面跑一些 Kubernetes 之类的自己用来测试的服务。
整体效果如下
从上往下:
但是在现有结构下,还存在一些问题
基于这样一些逻辑,我考虑进行如下改造
基于上面的一些考虑,在经过多方面选择后,我选择 UBNT 作为我的接入设备
整体的理由如下:
所以最开始设备的选择如下
然后整体的效果如下
但是在使用几天后,我发现了新的问题:我对容量的预估严重不足!
实际上千兆交换机理论吞吐是 1Gbps,但是实际上来说,刨除协议开销,有效负荷的速率大概在 940Mbps 左右。而这一点实际上对我现在的一些使用场景是有所不足的。比如在 PC 端转码然后回传 NAS 之类的。
而我目前又没有 10Gbps 常态化的传输需求。经过慎重考虑后,我决定将家中局域网的上限提升至 2.5Gbps。这样能达成性能与易用性的平衡点
那么我的设备选择如下、
整体效果如下
当然中间穿插的小插曲是,在升级 2000M/200M 宽带后,我将家里的光猫替换成猫棒直接在主路由上接入光纤
然后我在网络上划分了三个 vlan
最后整体的网速差不多是这样
整体的拓扑如下
差不多就是这样
经过改造,我相信这样的整体结构能够满足我使用很长时间。当然估计有很多人不理解为啥我租房还要这么折腾。我想除了职业习惯的原因以外,也还是想给自己的日常生活找点乐子。毕竟房子是租的,但是生活不是。
]]>实际上 IaC 的历史其实足够悠久。首先来看一下 IaC 的核心的特征
实际上通过 IaC 这样的一些核心特征,我们现在能明白 IaC 兴起的原因。IaC 实际上的兴起,大背景是在千禧年之后,互联网世界迭代的速度愈发的快速,这个时候传统的手工式的维护面临着几个问题
在这样的时代背景下,大家都在追求用更技术,更优雅的手段来解决这些问题。于是,IaC 这个概念就出现了
如果说要将 IaC 分为几个阶段的话,那么我觉得可以分为以下几个阶段
如同前面所说,IaC 实际上是一个自发的驱动,在面对不确定的时候,我们选择用代码来尽可能的消泯掉不确定性(实际上这个原则一直贯穿到现在)
那么在最早期,人们选择用最基础的代码的形式,来完成 IaC 的工作。其特征是对于之前的各种交互式的手段的精确化,程序化的描述。人们可能会选择直接用 bash 来解决这一切(祖传的来路不明的 bash 脚本.jpg),也可能会基于 Python Fabric 这样的框架进行简单的封装来完成所需的程序化描述的工作。
但是我们回头去看这一阶段,我们能直观的感受到一些缺陷
所以在面对这样一套的问题的时候。更现代化的 IaC 设施应运而生。其中典型的一些产物是
实际上这些工具,可能设计上各有所取舍(比如 Pull/Push 模型的取舍),但是其核心的特征不会变化
那么截至到现在,实际上 IaC 的发展其实到了一个相对完备的程度。其中不少工具,也依旧贯穿到了现在。
从2006年8月25日,Amazon 正式宣布提供了 EC2 服务开始。整个基础设施开始快步向 Cloud 时代迈进。截止到目前,各家云厂商提供了各种各样的服务。通过十多年的演进,也诞生出了诸如 IaaS,PaaS,DaaS,FaaS 等等各种各样的服务模式。这些服务模式,让我们的基础设施的构建,变得更加的简单,更加的快速。但是这些服务模式,也带来了一些新的问题。
可能写到这里,有同学已经能意识到了问题的所在:在获取算力,获取资源越来越快捷的当下。我们怎么样去管理这样一些资源?
那么要解决这样的问题,我们似乎又需要去考虑怎么样用代码或者可声明式的配置来管理这些资源。有没有一点眼熟,历史始终就是一个圈圈.jpg
在起初的时候,我们各自会选择基于各家云厂商提供的 API 与 SDK 自行封装一套 IaC 工具,如同前面所说的一样。这样会带来一些额外的问题:
那么这个时候,云时代的,面向云资源管理的新型 IaC 工具的需求也愈发的迫切。这个时候,Terraform 这样的新型工具应运而生
在 Terraform 里,可能一台 EC2 Instance 的开启可能就是这样的一段简短的定义
1 | resource "aws_vpc" "my_vpc" { |
这个基础上,我们可以继续将我们诸如 Database,Redis,MQ 等基础设施都进行代码化/描述式配置化,进而提升我们对资源维护的有效性。
同时,随着各家 SaaS 的发展,研发人员也尝试着将这些 SaaS 服务也进行代码化/描述式配置化。以 Terraform 为例,我们可以通过 Terraform 的 Provider 来进行对接。比如 newrelic 提供的 Provider,Bytebase 提供的 Provider 等等
同时,在 IaC 工具帮助我们完成基础设施描述的标准化之后,我们在此基础上能做更多有趣的事情。比如我们可以基于 Infracost 来计算每次资源变更所带来的资源花费变更。基于 atlantis 来完成集中式的资源变更等等进阶的工作。
那么到现在为止,我们已有的 IaC 产品的选择足够多,能满足我们大部分需求。那么是不是 IaC 整个产品的发展实际上就已经到了一个相对完备的程度呢?答案很明显是否定的
所以这张主要来聊聊当下 IaC 产品所面临的一些问题,以及我对未来的一些思考吧
先给大家看一个例子
1 | locals { |
这段 TF 配置描述虽然看起来长,但是实际上做的事很简单,根据不同的域名 *.manjusaka.me
将流量转发到不同的 instance 上。然后对于 demo0.manjusaka.me
这个域名,进行单独的流量灰度处理。
我们能发现,Terrafrom 这种 DSL 的解决方案所需要面临的问题就是在对于这种动态灵活的场景下,其表达能力将会有很大的局限性。
社区也充分意识到了这个问题。所以类似 Pulumi 这种基于 Python/Lua/Go/TS 等完整的编程语言的 IaC 产品就应运而生了。比如我们用 Pulumi + Python 改写上面的例子(此处由 ChatGPT 提供技术支持)
1 | from pulumi_aws import alb |
你看,整体的用法是不是更贴近于我们的使用习惯,其表达力也更好
实际上在云时代的 IaC 工具,更多的去解决的是基础设施的存在性的问题。而对于已有基础设施的编排与更合理的利用实际上是存在比较大的 Gap 的。我们怎么样将应用部署到这些基础资源上。怎么样去调度这些资源。实际上是个很值得玩味的一个问题。
实际上可能出乎人们的意料,实际上 Kubernetes/Nomad 实际上就在尝试解决这样的问题。可能有人在思考,什么?这个也算是 IaC 工具?毫无疑问的是嘛,不信你对照一下我们前面列的 IaC 的几个核心特征
同时我们在对应的配置文件里,可以声明我们所需要 CPU/Mem,需要的磁盘/远程盘,需要的网关等。同时这一套框架实际上将计算 Infra 进行了一个相对通用性的抽象,让业务百分之八十的场景下并不需要去考虑底层 Infra 的细节。
但是实际上这套已经存在的方案又会存在一些问题。比如其复杂度的飙升,self-hosted 的运维成本,以及一些抽象泄漏带来的问题。
云时代新生的 IaC,其 scope 相较于传统的诸如 ansible 之类的 IaC 工具范围更大,野心也更大。所带来的副作用就是其质量的偏差。这个话题可以分为两方面说
第一点来看,诸如 Terraform 这样的 IaC 工具,通过官方提供的 Provider 实现了对 AWS/Azure/GCP 等平台的支持。但是即便是官方支持,其 Provider 里设计的一些逻辑,和平台侧在交互式界面里的设计逻辑并不一致。比如我之前吐槽过“比如 Aurora DB Instance 的 delete protection 在 Console 创建时默认打开,而 TF 里是默认关闭”。这实际上会在使用的时候,给开发者带来额外的心智负担
第二点来看,IaC 工具极度依赖社区(此处的社区饱含开源社区和各类商业公司)。不同于 Ansible 等老前辈,其周边设施的质量相对稳定。Terraform 等新生代的 IaC 周边的质量一言难尽。比如国内诸如福报云,华为云,腾讯云等厂商提供的 Provider 一直被人诟病。而不少大型的面向研发者的 SaaS 平台没有官方提供的 Provider 等(比如 Newrelic)
同时,云厂商所提供的一些功能实际上是和通用性 IaC 工具所冲突的。比如 AWS 的 WAF 工具,其中有一个功能是基于 IPSet 进行拦截,这个时候如果 IPSet 非常大,那么使用通用性的 IaC 工具进行描述将会是一个灾难性的存在。这个时候对于类似的场景,只能基于云厂商自己的 SDK 进行封装,云厂商提供的 SDK 质量合格还好。如果像福报云这样的神奇的 SDK 设计的话,那就只能自求多福了。。
开发者体验实际上现在是一个比较热门的话题。毕竟没有人愿意将自己宝贵的生命来做重复的工作。就目前而言,主要的 IaC 工具都是 For Production Server 的,而不是 For Developer Experience 的,导致我们用的时候,其体验就很一般。
比如我们现在有一个场景,我们需要在 AWS 上给研发的同学批量开一批 EC2 Instance 作为开发机。怎么样保证研发同学在这些机器上开箱即用,就是很大的一个问题。
虽然我们可以通过预制镜像等方式提供相对统一的环境。不过我们可能会需要更进一步的去细调环境的话,那么就会比较蛋疼。
针对于类似的场景,老一点的有 Nix,新一点的有 envd 来解决这样一些问题。但是目前来讲,还是和已有的 IaC 产品有一些 gap。后续怎么样进行对接可能会是个很有趣的话题。
最典型的是 Serverless 的场景。比如我举个例子,我现在有个简单的需求,就是用 Lambda 来实现一个简单的 SSR 的渲染
1 | export default function BlogPosts({ posts }) { |
函数本身非常简单,但是如果我们要将这个函数部署到 Production Enviorment 里将会是一个比较麻烦的事。比如我们来思考下我们现在需要为这个简单的函数准备什么样的 infra
那么在 IaC Manifest + 业务代码彼此分离的情况下,我们的变更以及资源的管理将会是一个很大的问题。Vercel 在最近的 Blog Framework-defined infrastructure 也描述了这样的问题。我们怎么样能进一步发展为 Domain Code as Infrastructure 将会是未来的一个挑战
这篇文章写了两天,差不多作为自己对于 IaC 这个事物的一些碎碎念(而不是 Terraform Tutorial!(逃。祝大家读的开心
]]>这个重构项目如果从我第一个超大型重构 PR 算起(22年12月11日),到现在已经历史一个半月了。目前重构进度已经超过了 80%,超过6+位贡献者集体贡献。这绝对是个不小的工程了
那问题来了,我为什么要发起这个重构项目呢?
在重构项目之前,nerdctl 项目存在一个很大的问题,即 command 的入口处,flag 的处理和逻辑耦合的问题,比如用 nerdctl apparmor
系列的代码来举一个例子
1 | package main |
你能看到在函数 apparmorLsAction
的逻辑中包含了两个部分的东西
这样的设计存在很明显的问题
同时 nercdctl 还存在另外一个问题。在 cmd 的入口处,因为同归属于一个 sub package,于是之前的开发过程中为了省事,文件之间为了省事,交叉引用了彼此的 internal helper function
在 nerdctl 项目最开始只作为 containerd CLI 的一个替代品的时候。之前的设计缺陷实际上暴露的并不明显。但是 nerdctl 完整提供了一套基于 containerd 的容器生命周期及网络管理(base on CNI)及其余进阶特性(比如 cosign,IPFS 等),开始作为 containerd 实质上的一个入口标准的时候。社区无疑会提出更高的需求。比如 Move *.go files for subcommand out main package nerdctl#1631 就是一个很典型的例子。
在这种情况下,对于 nerdctl 的入口进行一个合理的但是大范围的重构,就是一个必须且迫在眉睫的事了。
又到了
白色相薄重构的季节 —- 蛮久抚子(Nadeshiko Manju)
好了,社区有需要,saka 哦不,蛮久抚子(Nadeshiko Manju)我就得站出来了,重构嘛,很简单嘛,Goland 搞一搞就完事了嘛。好说好说。于是我有了一个超大的 PR :Refactor the package structure in cmd/nerdctl nerdctl#1639。规模 +5000 -4000
不过,因为这个 PR 太过于惊世骇俗,在我 COVID-19 Positive 后,Suda 开始帮我 carry 这个 PR。但是最后 Suda 也高呼不可 carry(Suda の惊く:ばか saka!)
どうしてこうなるんだろう…初めて、リファクタリングしたいという欲求があり、リファクタリングの必要性がありました。嬉しいことが二つ重なって。その二つの嬉しさが、また、たくさんの嬉しさを連れてきてくれて。夢のように幸せな時間を手に入れたはずなのに…なのに、どうして、こうなっちゃうんだろう…
为什么会变成这样呢,第一次有了想重构的欲望,又有了重构的必要。两件快乐事情重合在一起。而这两份快乐,又给我带来更多的快乐。得到的,本该是像梦境一般幸福的时间……但是,为什么,会变成这样呢…… —— 《nerdctl 相薄》
实际上原因很简单 冬马小三 ,哦不是,是我小三,哦,不是,是我脑子被门夹了
言归正传,其实这个 PR 是个教科书式的反面例子
所以在吸取了 Refactor the package structure in cmd/nerdctl nerdctl#1639 的教训后,我正式在社区提出了一个重构 Proposal Let’s refactor the nerdctl CLI package nerdctl#1680 ,在这个 Proposal 中我做了几个事情
社区其余几位 maintainer 在这个 Proposal 下额外讨论了一些细节,并达成了一些共识
截止到现在,nerdctl 的重构才算开始正式进入了一个快车道的状态。毕竟重构不是乱写,要是写错了,要向社区谢罪的。
这里面其实还有个插曲,最开始我在 Issue 中创建 TODO Task 之后,为了方便 track project 的进度,我将这些 TODO Task 直接全部转成了 Issue(然后就相当于给 subscribe 了这个 repo 的老哥们来了一个邮箱 DDOS)。这里不得不吐槽一句,GitHub 的项目管理工具真的很弱诶(XDDDDD
花开两朵,各表一只,在 Proposal 正式通过了之后,整体的重构就开始进入了快车道了,这里列一些有意思的讨论,大家有兴趣可以去看看
当然还有很多 PR 中的讨论也是非常有意思的,这里就不完整列出来了。欢迎大家去直接看原始的 PR(当然欢迎加入讨论)
差不多就这样吧,大概复盘了一下到现在为止重构过程中的得失。希望大家能喜欢
]]>从2021年11月第一次发起刷题公益计划,到现在也一年多时间了。起初是为了让大家有一些特殊的动力去刷题,所以有了这样的基础规则
基金池最开始由群主承担,后续有超过25位+群友集体捐款
再后来,这个群就发展成了基于技术的各种闲聊群,推荐番毒害群友群。
到目前也差不多一年多时间了,写个简报回顾一下
截至目前,从2021年11月开始,到2022年6月作为一个阶段的结束。
在2022年6月,经过群友同意,再经过一轮扩资后,蓝莲花小组向一个村小项目捐款 6000 元人民币
前不久得到反馈,这笔钱已经用在应该用的地方了。开心
从2022年6月开始,群友决定在群内以一周两次的频率进行分享,截至目前举行了八次分享
截至目前,群友的足迹包括不仅限于
2022 年群内也新诞生了两位开源项目的 maintainer
时间过的很快,转眼这个群就一年多了。很荣幸能在这个浮躁的时代里认识一些很纯粹的人。2023 一起加油
最后,愿每个人心中都能盛开着永不凋零的蓝莲花
]]>半夜接到群友求助,说自己的测试环境遇到了点问题,正好我还没睡,那就来看一下
问题的情况很简单,
用
docker run -d --env-file .oss_env --mount type=bind,src=/data1,dst=/cache {image}
启动了一个容器,然后发现在启动后业务代码报错,抛出 OSError: [Errno 28] No space left on device 的异常
这个问题其实很典型,但是最终排查出来的结果确实非典型的。不过排查思路其实应该是很典型的线上问题的一步步分析 root casue 的过程。希望能对看官就帮助
首先群友提供了第一个关键信息,空间有余量,但是就 OSError: [Errno 28] No space left on device 。那么熟悉 Linux 的同学可能第一步的排查工作就是排查对应的 inode 情况
执行命令
1 | df -ih |
我们能看到 /data1 实际上的 inode 和整机的 inode 数量都是足够的(备注:这里是我自己在我自己的机器上复现问题的截图,第一步由群友完成,然后给我提供了信息)
那么我们继续排查,我们看到了我们使用了 mount bind1 的方式将宿主机的 /data1 挂载到了容器内部的 /cache 目录下, mount bind 可以用下面一张图来表示和 volume 的区别
都在不同版本的内核上,mount bind 的行为有一些特殊的情况,所以我们需要确认下 mount bind 的情况是否正确,我们用 fallocate2 来创建一个 1G 的文件,然后在容器内部查看文件的大小
1 | fallocate -l 10G /cache/test |
文件创建没有问题,实际上我们就可以排除掉 mount bind 的缺陷了
接着,群友提供了这个盘是云厂商的云盘(经过扩容),我让群友确认下是具体的 ESSD 还是 NAS 这种走 NFS 挂载的 Block Device(这块也有坑)。确认是标准的 ESSD 后进入下一步(驱动的问题可以先排除)
接着,我们需要考虑 mount —bind 在跨文件系统情况下的问题。虽然前面一步我们成功创建了文件。但是为了保险起见,我们执行 fdisk -l
和 tune2fs -l
两个命令,来确认分区和文件系统的正确性,确认文件系统的类型都是 ext4,那么没有问题。具体两个命令的使用方式参见 fdisk3 和 tune2fs4
然后再回顾我们之前直接在 /cache
下创建问题没有问题,那么这个时候我们心里应该大概有底,这个应该不是代码问题,也不是权限问题(这一步我额外排除镜像的构建里没有额外的用户操作),那么我们需要排除一下扩容的问题。我们将 /data1 unmount 之后,重新 mount 后,再执行容器,发现问题依旧存在,那么我们就可以去排除扩容的问题了。
现在一些常见的问题已经基本排除,那么我们来考虑文件系统本身的问题。我登录到机器上,执行了以下两个操作
/cache/xxx/
下,我用 fallocate -l
创建一个报错的文件(长文件名),失败/cache/xxx/
下,我用 fallocate -l
创建一个短文件名),成功 OK,我们现在排查路径就往文件系统异常的方向上靠了,执行命令 dmesg5 查看内核日志,发现了如下错误
1 | [13155344.231942] EXT4-fs warning (device sdd): ext4_dx_add_entry:2461: Directory (ino: 3145729) index full, reach max htree level :2 |
OK,我们期待的异常信息找到了。原因是,ext4 基于的 BTree 索引,默认情况下只允许树的层高为2,实际上就大概限制了目录下的文件数量大概在 2k-3kw 以内。经过确认,这个问题目录下的确有大量小文件。我们再用 tune2fs -l
确认下是否是如我们猜想,得到结果
1 | Filesystem revision #: 1 (dynamic) |
bingo,的确没有开启 large_dir
的选项。那么我们执行 tune2fs -O large_dir /dev/sdd
开启这个选项,然后再次执行 tune2fs -l
确认下,发现已经开启了。然后我们再次执行容器,发现问题已经解决。
上面的问题排查看似告一段落。但是实际上并没有闭环。一个问题的闭环有两个特征
从上面 dmesg 的信息我们能定位到内核中的函数,其实现如下
1 | static int ext4_dx_add_entry(handle_t *handle, struct ext4_filename *fname, |
ext4_dx_add_entry
函数的主要功能是将新的目录项添加到目录索引中,我们能看到这段函数在 add_level && levels == ext4_dir_htree_level(sb)
这里检查对应的特性是否打开,以及当前 BTree 层高,如果超出限制,则返回 ENOSPC
即 ERROR 28
好了,在复现异常之前,我们来获取下这个函数的被调用路径。这里我用 eBPF 的 trace 来获取 stacktrace,因为与主体无关,我在这里就不放代码了
1 | ext4_dx_add_entry |
那么我们怎么验证这个是我们的异常呢
首先我们利用 eBPF + kretproble 来获取 ext4_dx_add_entry
的返回值,如果返回值是 ENOSPC
,则我们就可以确定这个是我们的异常
代码如下(不要问我这里为啥不用 Python 写,要写 C 了(
1 | from bcc import BPF |
然后我们写段很短的 Python 脚本
1 | import uuid |
然后我们看到执行结果
符合预期,那么我们可以说这个问题的排查路径的因果关系链完整了。那么我们也可以正式宣告解决了这个问题了
那么锦上添花的一点,对于这种上游的问题,我们如果能找到具体在什么时间点进行了修复,那就更好了。就这个 case 而言,ext4 的 large_dir 在 Linux 4.13 中得到引入,具体可以参见 88a399955a97fe58ddb2a46ca5d988caedac731b6 这个 commit。
OK 这个问题就告一段落
其实这个问题比较冷门,但是排查方式其实是挺典型的线上问题的排查方法。对于问题,不要预设结果,一步步的根据现象去逼近最终的结论。以及 eBPF 真的好东西,能帮助做很多内核的事。最后我的 Linux 文件系统方面的底子还是太薄弱了,希望后面能重点加强一下
差不多就这样
实际上每年都在觉得这一年很魔幻,但是下一年总会跳出来说“这一年更魔幻”。不过这也是人生的乐趣吧。
看了下20年总结的标题叫做”但行好事,莫问前程“,去年一下想不起标题,群内求助了下,发现”Stay Simple,Stay Naive“这个标题还不错,挺适合作为去年的总结与展望的。不过在写下这点文字的时候发现当年 +1S 的对象也已经仙去了。怎么说心里也还是有点很奇怪的感觉在里面。
不过,一日膜法,终身膜法,所以就还是 Naive 的 +1S 吧
去年从年初开始,我从太极图形离职后,就开始进入了我数字游民的生活。作为一个 FreeLancer,可能最大的好处就在于说免去了通勤的时间后,我可以有更多的时间做自己的事(睡大觉(不是
在离职之后,和女朋友一起换了一个新的房子,有着很大的落地窗的露台,采光很好,所以让我在这里有时间安心做一些自己的事情
所以去年在有自己的时间的情况下,我开始看之前没有怎么涉猎的杂书,印象比较深的有这样几本
整体的阅读量在20本左右吧。然后发现,你去慢慢找书,然后发现某个作者的风格很符合你的 XP 是件非常幸福的事。
在看书之余,我也开始看番了,这个行动一度占据了我 Q3/Q4 很多业余时间(导致我这段时间不去干其余事了(你们这个群害人不浅啊。当然,补剧,看纪录片,也都是这一年的一部分,过这对于我来说实际上也是全新的体验了。技术和睡觉之外的世界也是格外的大啊
然后家里新入职了两只猫咪,现在家里整整有六只猫,这对于我来说完全是幸福的烦恼。撸猫一时爽,一直撸猫一直爽。(当然铲屎和猫咪集体生病的时候就很不爽了。
当然好事说了这么多,当然要说点坏事了,去年的减肥计划执行的很不彻底,以及去年的运动计划也没有执行,呜呜呜呜呜。
从去年开始再次勇敢面对抑郁症的现实后,在药物和整体相对自由的环境的情况下,我自己的精神状态控制的也还不错。不过可能因为这一年是我被性侵到现在第十五个年头的缘故吧,去年的噩梦有点多,希望时间能继续治愈一切吧。(不过说起来,我讨厌药物副作用(真的让人很不爽
说回来,去年有了自己时间后,家里也添置了不少能极大提升自己生活幸福度的物件
另外一提的是,去年公益我也在继续坚持坐着,我自己累计捐款10k+,然后公益群的小伙伴一起凑了点钱给一个村小捐款6K+,另外一点非常开心的是,我也带动了身边的人,去捐助学生。教育是最好的公益.jpg。不知道还能坚持多久,但是还是做一些力所能及的事吧。
差不多是这样,2022 整体的生活也还算是有滋有味。不过心里还是会隐隐约约有点担心,在整体局势下行的情况下,我这样小确幸的生活又能持续多久呢?
感情步入了第四个年头,去年因为北京疫情的原因,和荆澈同学一起朝夕相处(这是真的朝夕相处)了一年了。用我很喜欢的《士兵突击》里的一句台词
常相守是个考验,随时随地,一生。
两个人朝夕相处,因为各种细节上的差异,一定会有一些小的争执与摩擦。这个时候就需要两个人相互包容。相互理解。说道这里我就很庆幸荆澈同学对我的包容与监督了。她经常碎碎念的督促我起床,督促我运动,督促我继续改掉我很多不好的习惯Hhhhhh(mua.jpg
很多时候,我半夜噩梦醒来,总会下意识的去抱着荆澈同学,她即便迷迷糊糊搞不清情况,也会转过来给我抱抱。某种意义上,荆澈同学的陪伴,是让我不断走下去的勇气的源泉
说回来,朝夕相处也未必是个坏事,去年和荆澈同学一起去公园散步,一起逛吃逛吃的时间多了很多。也一起去泡了温泉,一起去了环球影城(Remote 万岁!)。
希望 2023 年也和荆澈同学也能一起顺顺利利的走下去,完成对荆澈同学的承诺(我要有八块腹肌.jpg(以及去旅游,去做更多的手工艺品!
反正我一如既往的 感激并享受着荆澈同学的爱 。
首先聊聊我自己的变更,如前面所说,在22年初,因为自己的规划和身体的原因,我正式离开了太极(说实话挺舍不得这群同事的),正式开始了我数字游民的生活。目前来说,我依靠给一些客户做 SRE 方面的能力输出为生。这对于我来讲其实是个蛮大的挑战。因为我之前的定位其实更多的还是偏向于一个 Infra Developer,将 SRE 作为我正式工作方向,其实对于我来讲,也是开天辟地头一回了(感谢客户爸爸的信任
去年其实工作内容也发生了很大的转变,也让我更多的意识到了自己的不足。如果说自己之前是一个纯粹的 IC 的角色,那么去年我的工作内容的边界实际上有了不少的扩展。我需要去更多的考虑协调的有效性,体系化的建设。很多时候我都在笑称我自己这周写的文档可能比我写的代码还多了(XD
不过这对于我来说也是一个好事,思路的转变我相信会让我提升很多。
2022另外一个比较重大的改变就是从2022二月开始,在 Xuanwo 几位好友的启发下,我开始正式的以公开的方式,记录自己每周的生活与技术学习(我老板说会看我周报(摊手。这一点其实对于我自己来说,也是比较好的一个手段吧。用一个锚点,去约束自己的生活(面向周报有内容式学习(不是),去记录自己的一些感悟与心得(输出了不少稳定性与可观测性的东西。希望23年能继续坚持。
技术方面的话,去年的成长我自己觉得也还是比较明显的。一方面是在开源社区这块。年初因为寻找 Docker 替代品,机缘巧合之下开始为 nerdctl 做输出,6月被 Promote 成为 Reviewer,12月被 Promote 成为 Committer。这也是我比较深度的参与开源社区了。同时我自己也会和身边的好友去交流关于开源社区的东西,比如和 Xuanwo 一起聊聊他的 OpenDAL,和 GaoCe GG 一起聊聊/吐槽他的创业项目 envd (他时常因为我比他还看好这个项目而惊讶(这个项目真的是好项目啊!。我自己觉得这一年去给不同社区贡献代码,参与讨论,对于我自己的提升是全方位的,更明确的意识到自己的 naive,也接受来自不同人的帮助与指导。如同我之前在一篇文章中的感悟一样
从互联网诞生之初到现在,开源这一极具理想主义气质的行为事实上的改变了这个世界。世界各地的人都在开源的旗帜下,自由的挥发着自己的创意,尽情的一点点的改变着这个世界。有些时候想到我会有机会去参与到这样一个伟大的活动中,我会不由自主的颤栗。我很庆幸在我最初的职业生涯里就加入到了这个伟大的事业,我也希望我身边会有越来越多的人参与进来,一起挥洒着汗水,一起在这个操蛋但是又美好的世界里,找到自己心灵的应许之地。
另外一方面的话,去年在技术深度这块做的也还算 OK,继续在之前自己积累的可观测性和稳定性方面精进,系统性的提升自己的一些体系化的思考(抽象成方法论),也继续在内核和 eBPF 这块做一些有意思的工作(比如帮助人去做一些小的工具)。希望23年也能继续勇猛精进
说起来,去年有一个很大的收获不知道算不算技术这块的,姑且算吧。之前组建的刷题群在去年格外的活跃,大家一起刷题,一起捐款,一起推荐番祸害群主(不是),一起做开源(去年群内诞生了两位开源项目的 maintainer),我很多时候遇到各种事情的时候都会在群里和群友们一起吐槽和发泄。很多时候我自己在感叹,在这个人心浮躁的时代,能遇到这样一群热情又纯粹的人,实则人生幸事,当浮一大白(不过我肝不好,就以零度代酒干了这杯)
差不多就这些,去年也还零零碎碎的做了很多其余的工作,开始翻译人生第一本书,保持了每日一题,读了十多篇论文,组织了好几次群内分享,写自己的 toy,很多很多。很多人觉得程序员是个很枯燥的行业。但是说实话,这一行真的让人迷醉
对比了下年初的目标,然后自评了一下差不多能给自己个3.5的绩效吧
优点和缺点都比较明显,聊聊缺点吧
缺点和改进方向还是比较明确的,希望明年继续努力。我自己目前列好的一些 OKR 差不多是这样
2022 实际上真的挺魔幻的,不过套用狄更斯的一句老掉牙的话
这是最坏的一年,这也是最好的一年
说实话我也不知道23年会怎么样,未来几年会怎么样。不过无论怎么样,爱与希望总是会支撑我们走过一年年。嗯,Everything is gonna be OK.
说起来,今年有人问过我我想成为一个怎么样的人,我想了下,这么回答到
我希望身边的人在很多年后,和老头老太太聊天或者给自己孙子提到我的时候会这么说”我之前认识一个叫 saka 的人,是个还不错的人“,那么我心满意足了
Stay Simple,Stay Naive,永远谦逊,敬畏生活,勇敢前行
再见 2022,你好 2023
]]>我参与的第一个开源项目,应该是能追溯到16年,我还没有本科毕业的时候,当时的我参加了 稀土掘金翻译计划(slogan 里说的最好的英文技术资讯翻译项目,我觉得毫不夸张),在这个项目里我第一次接触到了 Git Workflow,也完整接触到了 GitHub 这个世界最大的同性交友社区(大雾(不过我相交至今对我帮助巨大的几位密友真的是通过这个项目结识的)。而我第一个参与的代码项目,应该可以追溯到17年3月,我给 Sanic 这个项目新增了一个 Code Example,参见 Sanic#PR558。
在往后,我就一直在不断的参与开源社区,到现在为止,我贡献过不少的开源项目,CPython,Docker/Moby,Taichi,Logseq,Kubernetes,Dubbo,TiDB,nerdctl 等等。我也在不断的学习开源社区的工作方式,我也在不断的学习开源社区的文化,我也在不断的学习开源社区的技术。(最后面这句由 GitHub Copilot 自动完成)(XD
那么回到这一章的标题,我为什么会参与开源社区?或者更功利的说,开源社区给我带来了什么样的利益?
无他,对于我自己全方位的成长。
首先,参与开源社区对于我来讲,对于我自己是一个非常非常棒的提升的过程。你可以在这里面学到很多的东西
更早的链接先不谈,大家可以看我 2022 年在 nerdctl 项目上的贡献 nerdctl#ZheaoLi,大家可以很明显的看到,我的 PR 从最开始到后面,无论是质量,还是风格都有不少的提升。这实际上就是开源社区所带给我的最直观的成长。我很庆幸有很棒的 Community Mentor 对我的 PR 从不放水,Review 非常严格,促使我不断的成长。
同时,让我也有机会去表达自己的想法,去发起 Proposal(比如 nerdctl#Issue1387),去学会做一个 Owner,去帮助更多的新人参与进来。
某种意义上,这是日常的工作所给予不了我的特殊的体验,开源社区相对较少的利益纠葛,会让互利互惠的行为变得更纯粹,更加的自然。也会让人收益更大。这里引用 @yuchanns 今晚的一段发言
我想大家刚学编程的时候都会有这种困境:学完不知道干啥、感觉好像没学,所以就想寻找各种实战教程来加深体会。
这种现象会在实际从事工作后迅速消除,因为有了实际应用场景。
但是当你对一些其他领域的东西产生兴趣,又会有这种困惑;而这是工作中不太有机会接触到的东西。除非你换了个工作、不然没法再通过工作经验来摆脱困境。
这时候参与到一个开放式的社区就很好了。其他人的工作中产生的需求给你提供了实战机((
你不需要自己一一涉足到具体的工作中,只要解决他们延伸出来的需要,就可以有机会运用学到的东西((
当然,从功利的角度来说,积极的参与开源社区,你能认识很多有意思的人,让你职业生涯更为顺利也是能给你带来的好处就是了(
参与开源社区无外乎有两种途径,
我主要会讨论下后者
很多人会给出开源三问 “我想参与开源社区,但是我不知道怎么做”,“我想参与开源社区,但是我不知道怎么找到一个项目”,“我想参与开源社区,但是我太菜了怎么办啊”
实际上这些问题解决起来都是没有你想象的那么困难,可能只是需要一点行动能力加一点好奇心。
实际上发展到现在,开源社区已经极其的庞大了,无论你的技术栈是什么,你都能找到合适的项目去参与。而且,开源社区的参与门槛也越来越低了,你不需要去了解整个项目的代码,你只需要去了解项目的 Issue,然后去解决这些 Issue,就可以参与到开源社区中来了。那么 How to find a project to contribute to ?
我自己的途径有两个
nerdctl 这个项目实际上的来源就是当时好友 @Junnplus 在推上的推广
然后我去看了下这个项目的定位,发现这个项目实际上戳中了我的痛点,于是我就开始在自己的环境中使用这个项目。
实际上去找到你感兴趣的项目实际上不是一件难事,可能只是需要一点点好奇心
那么,我找到一个项目后,我应该怎么样去参与进去?
实际上这里就需要一点行动力了,我自己大概方法是这样
以 nerdctl 为例,Issue 区时不时的会有 Good First Issue 的出现,这个时候你可以主动的去认领对应的 Issue 进行贡献(从我的视角来看,项目的维护者对于 Good First Issue 的上心程度将会决定了一个项目的长远发展),@yuchanns 第一个 PR nerdctl#PR1331 实际上就来源于我提的一个 Good First Issue nerdctl#1330。当然对于一个已经有一定规模的项目来说,坐着等 Good First Issue 可能需要点运气,那么怎么办,答案就是第二,第三点
我在 nerdctl 第一个贡献的 PR nerdctl#PR790 来自于我提出的 Issue nerdctl#Issue775 ,这个 Issue 是我在使用过程中发现的 Bug,简而言之就是在私有镜像仓库下鉴权的一些问题。然后将 Issue 转化成对应的 PR 了。我在这个项目中其余的一些贡献也是修我自己遇到的一些问题
另外一个方法是,我会用我已有的知识去进行迁移,尝试是否能发现有潜在的问题。我在 Affine(一个非常棒的笔记项目)提的 PR Affine#PR403 是我在本地构建 Affine 的时候,顺手读了一下他们的 Dockerfile(我是 SRE,对这个比较敏感(不然前端项目我去读 Dockerfile 干嘛),发现他们没有高效的利用缓存,然后我就提了 PR,进行了构建加速。这是实际上就是跨领域的去看一个项目能给你带来不一样的视角,进而促进你对项目的贡献。
那么,开源三问最后一问,”我想参与开源社区,但是我太菜了怎么办啊“
首先要说一点,开源社区的精髓就在于边做边学边成长,比如 @yuchanns 在写 nerdctl#PR1407 的时候(这个 PR 主要是给容器新增一个可以绑定 MacAddress 的选项),他当时对于 CNI 这块也不是很熟悉,然后边做边学,我和他也在群里讨论过几次方案。最终 PR 合并的非常顺利。这某种意义上也是开源社区的一种乐趣与魅力。
那如果你说你现在就是背景知识不够,你想等再学学再写代码,那还能贡献吗?可以啊,用 @tison 的经典言论”一个社区的活绝对是很多样的“。你看,我给 bytebase 提 Bug 的时候,发现他们的 Ticket 模板太难用了,然后我交了Bytebase#PR3050 重构了他们的 Issue Template,后面他们基于我的基础上又完善了一波。所以,无论是 Issue,文档完善,帮助完善用例等,都是很棒的参与开源社区的方式。
当然可能新进来的同学还有个顾虑就是如果被拒绝了怎么办?那其实很常见,你看我拍脑袋给 lima 提的 lima#Issue1087 被拒的很惨。但是被拒绝也是一种学习,能让我自己从这个讨论的过程里去回顾到我思考不完善的地方。
所以看到这,你会发现,参与开源社区,真的没有那么难。需要的真的只是一点点行动力,以及一点点的好奇心而已
从互联网诞生之初到现在,开源这一极具理想主义气质的行为事实上的改变了这个世界。世界各地的人都在开源的旗帜下,自由的挥发着自己的创意,尽情的一点点的改变着这个世界。有些时候想到我会有机会去参与到这样一个伟大的活动中,我会不由自主的颤栗。我很庆幸在我最初的职业生涯里就加入到了这个伟大的事业,我也希望我身边会有越来越多的人参与进来,一起挥洒着汗水,一起在这个操蛋但是又美好的世界里,找到自己心灵的应许之地。
Long Live the Open Source!
]]>