从男孩到父亲,不过几届世界杯

童年

作为一个晚熟的男孩,对初中前的记忆都很模糊,尤其是02年之后来了上海这座新城市读初中,一下子把自己的童年都切割了,但是自始至终有一件事情的记忆无比清晰,那就是足球。

小学那会儿还是中国足球氛围最浓的时候,和同学放学后的活动就是在球场上踢球玩,还记得我妈给我报了足球班,中暑了之后仍然说要去,踢球反复蹭破膝盖导致伤口化脓还是要继续踢;作为一个语文渣,甚至一个人在家玩球,还写了小作文——左右脚足球赛上了报纸。

2002韩日世界杯决赛,横滨国际体育场,巴西2-0德国,罗纳尔多补射破门

02年韩日世界杯是我有印象的第一届世界杯,中国队第一次进入世界杯决赛圈,所有学校都停课看比赛。后来的事情大家都知道了,本以为只是国足辉煌的开始,没想到已是巅峰。也是那个时候,我有了自己的足球主队——巴西,最终3R组合威风八面,巴西队7战全胜,第五次加冕。毫不夸张的说,当年真的是满城尽是黄衫9号(字面意义上),所有人都被罗纳尔多给征服。时至今日,罗尼也成了我唯一追过的球星。

到上海之后,这边的同学没人踢球,一下子失去了环境,毕竟还在读书,看电视的机会也不多,只能通过买足球周刊、看足球之夜、天下足球来了解比赛,按现在的话来说,慢慢地变成了一个云球迷。其实在02年的时候自己还是小城市里的小朋友,没有信息渠道,对球星知道的非常少,但是通过足球周刊、足球节目了解了更大范围的足球世界,比如知道了罗尼的传奇,才知道他02年世界杯前的几年因为连续重伤根本就没打几场球,没有人看好巴西队,也很少人会猜到他能够单届世界杯进8球。

2006德国世界杯,巴西队的7号8号9号和10号,纸面实力爆炸

到了06年德国世界杯,正好轮到我中考结束,所以可以毫无顾忌的看球。那一年的巴西真的是纸面实力无敌,罗纳尔多+罗纳尔迪尼奥+卡卡+阿德里亚诺的魔幻四重奏,再加上前一年联合会杯和美洲杯的夺冠,所有人都认为巴西能连续四届世界杯进入决赛,第六次捧杯,罗纳尔多将比肩贝利的时候,巴西再一次倒在了法国脚下。我还记得那一年我一个人在小房间郁闷了很久,那也是我第一次也是唯一一次为足球落泪,更让我没想到的是这场比赛竟然就是罗尼的国家队绝唱。

渐渐发福的罗尼职业生涯也彻底走上了下坡路,所有人都在或感叹巴西天才集体早衰、抨击他们不自律,作为一个热血少年当时也和现在很多小朋友一样,喜欢在贴吧和人对喷。虽然嘴巴上不承认,但是不得不接受的事实就是巴西队的天才球员越来越少了,而且世界足坛的中心已经彻底到了欧洲,欧冠联赛的重要性越来越大,所有球员必须接受欧化的踢球思路。

2009联合会杯决赛,巴西3-2逆转美国,队长卢西奥打入反超进球激动不已

大学

到了10年南非世界杯巴西的核心是踢球风格很欧洲的卡卡,而主教练是老顽固邓加,邓加虽然带领巴西拿了前一年的联合会杯,记得联合会杯决赛巴西0比2落后,最后连追3球时我激动不已。不过总体上来说,邓加太过务实的风格实在是让人喜欢不起来,并且他没有带上当时已经在巴西国内出名的内马尔,我心里是骂了他一万遍。最终这支想欧欧不动的球队同样在8强被淘汰,而那场比赛也和四年前一样完全让人摸不着头脑,上半场巴西基本上压着荷兰打,梅洛一脚妙到毫巅的直塞帮助巴西取得领先,没想到下半场他就变成了罪人,巴西被一个乌龙和角球破门送回了家,邓加随即下课。

2010南非世界杯,巴西1-2荷兰,梅洛被红牌罚下

邓加固执的用人没有让年轻球员的得到充分的锻炼,这也让巴西足球在很长一段时间内经历了人员荒,11美洲杯淘汰赛伊始就输给巴拉圭,巴西进入了彻底的低谷,不过看了多年球的我已经能够平静地接受这一事实。直到13年联合会杯,巴西只能凭借东道主的身份勉强参加,我都不是很看好这支年轻的桑巴军团能夺冠,然而我又被啪啪打脸了,不过决赛场上3-0完爆江河日下的西班牙,事后看来或许只是助长了二进宫的大菲尔对落后的战术体系的坚持。

2013联合会杯决赛,巴西3-0西班牙,内马尔爆射进球

大学里由于时间相对自由,所以也是我看球最多的一段时间,绝代双骄的同场竞技固然精彩,可能因为我是巴西球迷,比赛看的越多,就越找不回曾经的看球激情。直到内马尔登录欧洲,纯正的桑巴风格真的让人眼前一亮,敢在场上做动作,当然受到对手的“照顾”也多,欧洲人自然是不喜欢这种风格,但正所谓千金难买爷乐意,喜欢巴西的人看球不就是好这口吗?

在大学校园可以用校园网,而校园网有个好处就是PT下载非常快,所以当时我加各种PT站,开始慢慢搜集历年来巴西国家队的比赛视频,到了14年巴西世界杯前夕,我基本上收集齐了94年世界杯以来每一届大赛的比赛资源,巴迷社区论坛里的人应该都下过我共享的资源。

2014巴西世界杯,巴西1-7德国赛后的金杯爷爷

14年巴西本土世界杯,所有巴西球迷都很期待内马尔的第一届世界杯表现如何,但是说实话,我还是预期不高的,没想到小组赛内马尔就整进去四个球,而且竟然突破了8强,半决赛对阵德国,虽然当时内马尔和队长蒂亚戈席尔瓦都上不了,我还是觉得只要保守一点应该有机会,最后上半场0-5的时候我气得砸桌子,砸到小拇指都受伤了。那场比赛之后很长一段时间都没缓过来,比赛视频也不收集了。

到了工作之后,空闲时间大幅减少,兴趣爱好必须精简,而半夜看球实在是个奢侈的爱好,尤其是西甲和欧冠那阴间的开球时间,只能看几场英超或者第二天找回放看看。梅西受伤,内马尔带队踢了10几场球那段时间的比赛看了不少,感觉这小伙儿能成大器,有球无球都能打,只可惜内马尔最灵动最巅峰的日子里没有遇到最好的巴西。俄罗斯世界杯南美区预选赛,前六场打完,踢得不知道什么东西(好像就赢了1场球),好在换帅蒂特之后一波连胜提前出线。

工作

18年俄罗斯世界杯是我离世界杯最近的一次,因为我去了现场,而且是和最爱的人一起!(参考这篇——2018俄罗斯世界杯vlog)我记得FIFA刚刚开始售票,连小组抽签都没有开始的时候,我就第一时间盲买了半决赛的票(从理性的角度分析来看,买的半区不对,因为东道主固定是A1,所以种子球队和他在同一个半区的概率是3/8),然后又买到了3-4名决赛的票,提前请了长假。后来出了抽签结果,我看见我的半决赛的票正好是巴西队的半区,兴奋了半天。

Beloved Brazil cannot enter final, Beloved girl will still marry me!

踏上俄罗斯的飞机前我已经穿上了巴西球衣,没想到“落地成盒”,刚一落地手机还没信号的时候,后座小伙已经告知我比利时淘汰了巴西的消息,顿时整个人都不好了,只能当来俄罗斯旅游了。不过有一说一,俄罗斯粗中有细的气质还是很令人着迷,而且物价低,非常值得去。世界杯决赛我们为了进莫斯科大学的fanfest,绕着走了很长很长的路,最后和一群重体味的欧洲老爷们摩肩擦踵的在广场上看完了决赛,只记得决赛一结束,就下起了倾盆大雨,那味道真的酸爽。

2018俄罗斯世界杯,天空被渲染成各种颜色的圣彼得堡体育场

生娃

2020年是一个有特殊意义的年份,因为新冠疫情的开始改变了世界上所有人的生活,也因为小柚子的出生,让我变成了一个父亲。没有人能料到2020之后的世界线会变成现在这般模样,现在想想18年和老婆去了俄罗斯,19年去了西葡自己还是很幸运的。

at 诺坎普

有了小柚子之后,爸爸的自由时间更是直接归零。既然这样,未竟的足球事业就交给她来做了,记得在小柚子很小的时候,家里就给她球玩,而且作为一个大运动发展超前的小朋友,很早就会走路跑步和踢球了,正好家楼下有大片草地可以带她跑一跑,每次看见别的小姑娘都穿着裙子在草地上玩,妈妈都会吐槽我们像个男孩子一样。

穿上巴西球衣的小柚子

到了2022年上海更是被封了几个月之久,既然去不了卡塔尔现场,那就在家看吧,世界杯氛围必须整起来,早早地在nike官网预定了巴西队22-23新款球衣和儿童版球衣,不得不说一家人穿着球衣走在路上那是相当的拉风。

20年的时间,巴西队从神坛跌落,最低的时候世界排名跌出过前十,又从低谷崛起,重新回到第一。

20年的时间,一代人心中的足球情人宣布退役,而看着他踢球长大的巴西孩子已经成为了新生代巴西球员心中的偶像。

20年的时间,天天在泥巴地里踢球的小男孩变成了带着一个小姑娘在草坪上奔跑的父亲。

回首望去,20年不过5届世界杯,只要看球热情还在,那个心中的小男孩也就还在。

小世界,大世界

题记:小朋友的大世界对大人而言可能只是一个小世界,但希望每个大人都可以在这个世界里找到乐趣,和小朋友共同开心。

出生之前

他和她刚刚结束蜜月长假,一起开心地制定着未来的世界奇观巡礼计划,仿佛全世界都在脚下。没过多久她怀孕了,在那一刻他还不清楚将要面对的是什么,因为他几乎没有自己小时候的任何记忆,或许迎来一个小生命不会对自己的生活有太大的改变吧,他侥幸地想。

孕期的她没有特别大的反应,每一次孕检都很正常,可能你就是小红书上晒的天使宝宝吧,他们仍然像往常一样可以吃吃喝喝逛逛,顶着大肚子还去旅游,去法喜寺许愿。

然而这个世界并不总是按照他愿意的方式运行,此刻的他还不知道接下来的几天他的世界将发生巨变,活动空间将局限在小小的病房内,不分白天与黑夜。

本来大家一致以为应该延后的产期由于羊水破裂不得不提前送进产房,他在外面焦急地踱步,产房外的丈夫数量从十几个变成几个,最后只剩下他一个。夜已经深了,突然被告知需要转剖,还有点懵的他对于医生之后的话只记住了几个关键词,然后颤颤巍巍签下了自己的名字,小窗的门就合上了,他赶到手术室门外,看着显示屏上那唯一的名字,他试图打开wiki搜索相关信息来平复自己的心情,然而他失败了,眼泪终究是无可抑制地流了下来。手术其实进行的很快,但是对于他而言那段时间似乎特别漫长,直到护士出现在窗口,告诉他大人小孩一切正常,他才松了一口气,是的,他成为一个爸爸了。

头五天

由于需要观察妈妈的盐水是否结束,所以爸爸一直没有睡觉,同时一直等啊等,直到早上六点多你才结束观察送到妈妈身旁,爸爸盯着你小小的脸庞看了许久,实在是没找出什么地方像自己,直到许多天以后爸爸回到家翻看自己刚出生的照片一对比才发现什么叫神似。

说实话,你刚出生的样子真的有点丑,但是护士都说你很厉害,一出生就会哭出眼泪了,而且非常会喝奶,趴在妈妈身上很快就掌握了诀窍,也没有把妈妈搞痛。都说小宝宝出生后头几天体重会下降,但是你第二天体重就不再下降了,真厉害!

由于疫情的原因,只允许一位家属陪护不允许更换,爸爸记得那几天的睡眠时间完全是挤出来的,感觉自己一度被搞的神经衰弱。但是到了出院的那天反而很兴奋,看着你一点一点被小包被包起来,抱着你走出医院大门的那一刻,爸爸感觉自己有望成为妈妈之后你最依靠的人了。

头三月

然而还是要相信科学,那么小的小朋友是完全不认识人的。一开始的两个月里,你的世界就局限在小床里,视线30cm的范围内,妈妈、外婆、月嫂阿姨才是你最亲近的人,爸爸很快就排不上号了。

到了三个月时候你好动的天性慢慢显露出来,抬头翻身均先人一步,你的世界已经不满足于小床了,无论在哪你都跃跃欲试。这个时候的你还很小只,爸爸可以把你放在大腿上给你做四肢的被动操,而你会用杠铃般的笑声和爸爸进行很简单的互动,由于之前剃了光头,从某些角度看你简直和爸爸小时候一模一样,以至于爸爸发了ins。

基因的力量

回顾这百天,爸爸发现你样样都好,只是有一个问题阻碍了你成为小红书里面的天使宝宝,那就是——睡渣,爸爸貌似只成功哄睡了你一次,但是那次你睡得很好,以至于爸爸奔走相告,激动万分。但是考虑到这时期的宝宝本身就无法避免夜奶,睡眠一般都是不太行,此时的爸爸妈妈还并不觉得有太大的问题。

会走之前

大运动超前的你很快就可以向前爬了,所以妈妈给你打造了一个自己的小天地,但是爸爸只想唱“柚子,柚子,你真了不得,小小围栏困不住你,爬出个运动员。”从一开始你就想往外走,还记得你第一次尝试往外爬的时候,你小短手小短腿腾在半空中,脸着地,非常不体面的完成越狱,奶奶竟然在旁边就眼睁睁的看着你,还在一边录像。

为了让你在围栏里的小世界有点乐趣,爸爸开始教你玩玩具,但是你总是掌握不了重点,所以爸爸也就开始想办法自己找乐子,比如用套圈圈的玩具教连大小都不知道的你排序算法,反而你听的很认真的样子,也不知道你长大以后还能不能从记忆深处捞出这些知识。

排序

你这个时候已经会无意识的叫爸爸妈妈,但是你最喜欢的还是是模仿一些没名堂的东西,比如爸爸妈妈在教你某些东西时候嘴里的一些拟声词你学得最快,一开始妈妈还不清楚这个基因是哪里来的,后来等到你姑婆们来家里,看到她们的口技的表演才知道原来这是许家祖传艺能。

和头三个月一样,你仍然是个睡渣,可是爸爸和妈妈都是睡觉高手,怎么也想不通,于是爸爸进入了焦虑期(口头上的),整天在家“你还不能睡整觉,怎么办哟”,直到十一的时候,你突然睡了几天整觉,爸爸以为你终于觉醒了,正要庆祝,结果发现这只是偶然现象,从此彻底接受你是个睡渣的事实,你爱咋地咋地吧。

学会走

由于爬爬垫有围栏,你8个多月的时候就能自己扶着围栏站起来,然后大人们再给你一些双手才能拿的东西,你就会无意识的双脱手,渐渐的就学会自己独立站立了;同时你扶着围栏的行动速度已经很快,不需要几秒钟就可以绕围栏一圈;而且根据爸爸长时间的观察,你的腿部力量已经足够,并且爷爷也直呼小脚蹬得有力量,完爆路上见到的软绵绵的小朋友,所有的一切都说明你已经具备了独立行走的前置条件。这个时候爸爸在围栏里面通过快速从一个角落移动到对角,就可以利用你想要过来找爸爸的特点训练你独立走路,一开始的时候你只走一两步马上就会意识到危险,然后去找最近的围栏扶住,这时候爸爸可以重新换一个角,你又会再试一次,没过两天,你已经可以几乎可以走一个完整的对角,只是由于心太贪,最后几步不愿意走,而是会直接扑到爸爸的怀里。

其实学会走路之前,小小的围栏世界就已经不太能满足你的探索欲望了,在学会走路之后你更是满屋飞奔,爷爷整天跟在你屁股后面怕你摔了,但是其实你走的很稳,防摔小书包买回来根本没用过几次,完全就可以走的很好。现在回想起来,爸爸妈妈似乎从来没有在你学走期间叮嘱你慢慢走,导致你就是比同龄的小朋友心大,没学会走就想跑说的就是你。

“一起”玩

不得不说小朋友看世界的方式真的和大人不一样,在妈妈为你打造的小世界里摆了各种经典的玩具,但是你一开始从来不按照大人们设想的方式去玩,而是有自己的理解。所以虽然说是一起玩,其实很多时候都是爸爸在你的小世界里玩自己的,然后你在旁边玩自己的,直到某个瞬间你可能会发现爸爸玩的有些意思才会来和爸爸互动。

比如家里的积木,在一开始爸爸试图教你如何搭形状,刚刚搭了一两块,你总是要来推倒,后来爸爸也放弃了,就开始自己想其他的玩法,有一次爸爸把各种形状的积木铺一层在圆形边框的积木盒的盖子上,目的是尽可能的利用空间在一层内多放置积木,你在旁边看爸爸是如何调整现有的积木位置再增加一块进去的过程,然后等到爸爸放第二层的时候你似乎也有点理解了,可以参与进来,自己进行这个过程。

最密堆积

还有就是妈妈给你买了一个六面都有常见的和旋转概念相关的物件(钥匙开锁、指尖陀螺、螺丝螺帽)的“忙碌块”,本意是希望在飞机和高铁上keep you busy,然而可能是你对这些物件的正确使用方式完全不能理解或者不理解这些行为的后果有什么意义,只会用蛮力,所以自然也得不到什么乐趣,至于那个keep you busy的意图也失败了。后来爸爸自己在玩这个忙碌块,倾斜忙碌块,让旋转的指尖陀螺打击到垂下来的钥匙,你可能观察到钥匙弹飞再落下来又弹飞,觉得很意思,于是你也开始来用手打击指尖陀螺让它保持旋转,之后理解了旋转概念的你,又学会了用一只手不停地拍打螺帽可以让它向上转出来,然后用另一只手拍打可以让它向下转进去。

忙碌块

当然更多的是那些无论如何也无法教会的玩具,比如教了你六个月的形状配对,不过现在回想起来,可能是爸爸自己也没有从中发现乐趣吧。

写在后面

转眼间这边文章拖到了你快18个月的时候才发,实在是因为爸爸太懒了。回想这18个月的旅程,爸爸对养育一个新生命到底对一个人意味着什么这个问题有了更多的思考,在陪着你、看着你的世界从小变大、和你“一起玩”的过程中,爸爸感觉仿佛自己也在一起长大,找到了童年的乐趣,真正的乐趣。

写在在爸爸三十岁生日到来之际

爸爸最近在整理自己和妈妈小时候的照片时,脑海中总是浮现一个有趣的父女对话场景。

女儿在看见爸爸小时候的照片时,好奇的问爸爸这个小男孩的故事,爸爸可能会这样诉说:

“有一个小男孩,小学的时候,他的世界是一个小小的足球场,即使中暑也要在球场上挥汗如雨。02年有他最完美的足球记忆,中国队第一次进入世界杯,巴西队七战功成,罗纳尔多8球金靴王者归来。

到了初中,他来到了大城市,小小屏幕上的算法题成为了他的另一个兴趣,即使题目做不出来被罚跑圈也愿意在机房刷oj,并乐此不疲。这时候的他有了自己的决策,并主动探索课本外更大的世界。

再后来,他进入了大学,各种各样的桌游成为了他新的兴趣,他从此走出了那个只能一个人玩电子游戏的孤独的小世界,可以在RK老师的拍卖游戏里体验勾心斗角,也可以在乌老师的农场里饿着肚子刷分;可以在权力的游戏的世界里尔虞我诈君临天下,也可以铁路游戏里一边经营铁路路线一边操纵公司股价体验商海沉浮。

之后他遇上了他一生的挚爱,他的兴趣就是和她一起做一切事情,话剧、音乐剧、画展、做陶艺,还有旅游:去香港坐天星小轮,在世界杯决赛的校园一起淋成落汤鸡,去伊比利亚度一个超长蜜月。”

“再然后呢?”

再然后的故事就回到了这篇文章的开头。

golang 下对时间漂移的故障注入

很多时候我们在做测试时候,需要对程序的时间或者定时器进行一些操纵,各个语言对于获取时间和定时器的实现都不尽相同,在网上找了一下,发现有一个chaos-mesh的项目里面提供了一个对golang和rust程序注入故障的方案。

要想能针对某个进程进行时间漂移的注入,首先要知道这个进程里面对于获取时间的调用是怎么进行的。对于golang程序而言,当我们调用time.Now()时,实际上是利用了vDSO (virtual dynamic shared object)机制,该机制可以让一些诸如gettimeofdayclock_gettime的系统调用更快,而golang的time.Now()或者golang的定时器实现中维护堆时使用的就是clock_gettime

知道了调用的函数,接下来就是需要想办法修改掉它,这里chaos-mesh使用的方案是通过ptrace去修改对应进程的内存空间,将clock_gettime的跳转改成一个自己的实现。

chaos-mesh还提供了一个测试程序的封装叫watchmaker,这个程序可以方便我们直接运行,只要直接输入pid和偏移量就可以完成注入。

主要功能的代码是func ModifyTime(pid int, deltaSec int64, deltaNsec int64, clockIdsMask uint64) error

大体流程如下

  1. 利用runtime.LockOSThread(),将当期goroutine和linux的thread绑定,这个我猜应该是防止该goroutine被切走,ptrace失效。
  2. ptrace.Trace(pid)attach到pid上。通过/proc/pid/task获取该进程的所有threads,并通过PtraceAttach syscall attach到每个tid上,同时通过/proc/pid/maps拿到该进程的虚拟内存映射信息。
  3. 通过关键字[vdso]找到vDSO entry的起始地址。
  4. 准备好一个fakeImage,里面直接写好了我们的跳转的程序,最后24 bytes是我们的三个参数。这块涉及汇编,没仔细看fakeImage里面的实现。
  5. 在当前进程查询到是否曾经注入过我们的fakeImage,如果没有那么通过mmap新map进去我们的fakeImage,否则找到我们之前的entry。把最后24 bytes改写为当前设置的参数。
  6. 找到vDSO entry中的clock_gettime的原始地址,这里需要利用golang的debug/elf包,来找到对应的symbol address。
  7. 把vDSO entry中的clock_gettime跳转修改掉。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // JumpToFakeFunc writes jmp instruction to jump to fake function
    func (p *TracedProgram) JumpToFakeFunc(originAddr uint64, targetAddr uint64) error {
    instructions := make([]byte, 16)

    // mov rax, targetAddr;
    // jmp rax ;
    instructions[0] = 0x48
    instructions[1] = 0xb8
    binary.LittleEndian.PutUint64(instructions[2:10], targetAddr)
    instructions[10] = 0xff
    instructions[11] = 0xe0

    return p.PtraceWriteSlice(originAddr, instructions)
    }

我们可以写个程序测试下效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// go build -o test test.go
func main() {
pid := os.Getpid()
if len(os.Args) > 1 {
// normal process
fmt.Println(pid, "start count", time.Now())
t := time.NewTicker(time.Second)
go func() {
for _ = range t.C {
fmt.Println(pid, "tick", time.Now())
}
}()
} else {
// time shift
fmt.Println(pid, "30s ticker", time.Now())
j := 0
t := time.NewTicker(time.Second*30)
go func() {
for tick := range t.C {
j++
fmt.Println(pid, "trigger tick", tick, "now", time.Now(), j)
}
}()
}
select {}
}

为了观察方便,同时起两个test进程,一个带参数,一个不带。
./test & ./test x &

如果正常运行,那么输出结果会是这样的:

1
2
3
4
5
6
7
8
99335 start count 2021-04-26 15:14:40.290945165 +0800 CST m=+0.000102053
99334 30s ticker 2021-04-26 15:14:40.290987246 +0800 CST m=+0.000068288
99335 tick 2021-04-26 15:14:41.291190225 +0800 CST m=+1.000347008
99335 tick 2021-04-26 15:14:42.291188665 +0800 CST m=+2.000345428
// 忽略当中的一些打印
99335 tick 2021-04-26 15:15:10.291344814 +0800 CST m=+30.000501519
99334 trigger tick 2021-04-26 15:15:10.291371963 +0800 CST m=+30.000452968 now 2021-04-26 15:15:10.291389026 +0800 CST m=+30.000470067 1 // 这是第一次30s定时器超时
99335 tick 2021-04-26 15:15:11.291317403 +0800 CST m=+31.000474367

我们先等到99334进程触发了一次定时器超时之后,我们立刻通过watchmaker工具注入一个30秒的漂移。
sudo ./bin/watchmaker -pid 99334 -sec_delta 30 -nsec_delta 0 -clk_ids "CLOCK_REALTIME,CLOCK_MONOTONIC"

注意如果我们只是想影响程序中time.Now(),只修改CLOCK_REALTIME就行了。这里我们需要影响定时器的超时,根据golang的定时器实现,定时器数据结构中取时间使用的是runtime·nanotime1,而对应的汇编实现里面取的是CLOCK_MONOTONIC,所以我们修改的clockid除了CLOCK_REALTIME还需要包含CLOCK_MONOTONIC

程序会输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
watchmaker Version: version.Info{GitVersion:"master-ge885846a41fd88", GitCommit:"e885846a41fd88aa3a46f8b318321b8e889312b9", BuildDate:"2021-04-13T08:09:23Z", GoVersion:"go1.14.14", Compiler:"gc", Platform:"linux/amd64"}
2021-04-26T15:15:18.764+0800 INFO zapr@v0.1.0/zapr.go:69 get clock ids mask {"mask": 3}
2021-04-26T15:15:18.764+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 attach successfully {"tid": 99334}
2021-04-26T15:15:18.764+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 attach successfully {"tid": 99337}
2021-04-26T15:15:18.764+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 attach successfully {"tid": 99339}
2021-04-26T15:15:18.764+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 attach successfully {"tid": 99341}
2021-04-26T15:15:18.764+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 attach successfully {"tid": 99343}
2021-04-26T15:15:18.765+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 detaching {"tid": 99334}
2021-04-26T15:15:18.765+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 detaching {"tid": 99337}
2021-04-26T15:15:18.765+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 detaching {"tid": 99339}
2021-04-26T15:15:18.765+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 detaching {"tid": 99341}
2021-04-26T15:15:18.765+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 detaching {"tid": 99343}
2021-04-26T15:15:18.765+0800 INFO ptrace zapr@v0.1.0/zapr.go:69 Successfully detach and rerun process {"pid": 99334}

这里也可以看到,一个普通的go程序也会启动很多线程。。。

然后回去看我们测试进程的输出变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
99335 tick 2021-04-26 15:15:10.291344814 +0800 CST m=+30.000501519
99334 trigger tick 2021-04-26 15:15:10.291371963 +0800 CST m=+30.000452968 now 2021-04-26 15:15:10.291389026 +0800 CST m=+30.000470067 1 // 这是上面第一次30s定时器超时
99335 tick 2021-04-26 15:15:11.291317403 +0800 CST m=+31.000474367
99335 tick 2021-04-26 15:15:12.291291676 +0800 CST m=+32.000448352
99335 tick 2021-04-26 15:15:13.291336311 +0800 CST m=+33.000493035
99335 tick 2021-04-26 15:15:14.291333735 +0800 CST m=+34.000490470
99335 tick 2021-04-26 15:15:15.29133939 +0800 CST m=+35.000496121
99335 tick 2021-04-26 15:15:16.291349531 +0800 CST m=+36.000506260
99335 tick 2021-04-26 15:15:17.291232264 +0800 CST m=+37.000389035
99335 tick 2021-04-26 15:15:18.291307312 +0800 CST m=+38.000464034
99334 trigger tick 2021-04-26 15:15:48.765121761 +0800 CST m=+68.474202738 now 2021-04-26 15:15:48.765129654 +0800 CST m=+68.474210641 2 // 由于我们修改了时间,里面触发了第二次的30s定时器超时
99335 tick 2021-04-26 15:15:19.291362269 +0800 CST m=+39.000519076 // 这里可以看见99335对比进程中的时间还是正常的

注意golang的默认的time这个数据结构的打印,m=+后面的数字就是进程启动后的monotinic时间,这里99334进程的monotinic时间一下子从38变成了68,所以立马触发了第二次超时。

至此,我们就完成了对一个golang进程的时间漂移的注入,赶快去测试下生产代码中是否有问题吧,据chaos-mesh团队的分享中所说,很多开源项目均有或大或小的问题哦。

golang 下反射 plugin 中的类型实例实现动态注入

设想一个场景,我们需要和其他团队配合一起开发,并且不想使用源码构建,也就是说最终希望通过集成对方发布的二进制的方式来完成部署。

那么我们有两种方式可以解决这个问题:

  • 定义进程间api,各自构建进程,通过进程间调用
  • 定义接口,实现者编译成动态库,运行时通过某些机制加载动态库然后再想办法获取到对应的实现的实例

对于c语言来说其实编译成动态库的方式是很自然的,但是golang的话就需要利用plugin机制。

首先我们需要使用go build -buildmode=plugin来将一个包含main包的代码编译成动态链接库。

1
2
p, _ := plugin.Open("plugin_name.so") // open so
f, _ := p.Lookup("routerFactory") // 查找符号

注意这里的plugin.Open并不会执行main包,而是仅仅会去调用所有包init函数。

p.Lookup("routerFactory")可以查找到名字为routerFactory的全局变量,我们可以要求实现者以一个固定的全局变量来实现我们的接口,接下来我们只需要断言到自己的interface,就可以调用了。注意,只有导出的变量或者函数符号才能被查找到。

如果我们想更加灵活一些,我们希望能查找未导出的符号,或者通过包名+类型名反射出实例,比如我们需要使用方法而不是全局的函数,那咋办。

这时候我们就要掏出抄代码大法了,go的reflect包内里面其实有这段代码typesByString

我们可以利用go:linkname来链接到reflect包内的实现,我们只用申明签名即可。注意使用这个需要import unsafe

1
2
//go:linkname typesByString reflect.typesByString
func typesByString(s string) []*_type

为了完整的实现,我们还需要从源码里面拷贝一些结构体和方法出来,比如type的定义,pkgpath()方法等,不用完整拷贝,调用的到的片段弄出来就行。

最后把这些骚代码封装到我们的internal包内,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//go:linkname typesByString reflect.typesByString
func typesByString(s string) []*_type

func searchForType(pkgPath, typeName string) (*_type, error) {
tbs := typesByString("*" + typeName)
for _, tb := range tbs {
p := (*ptrtype)(unsafe.Pointer(tb))
path := p.elem.pkgpath()
if path == pkgPath {
return &p.typ, nil
}
}
return nil, fmt.Errorf("%s, %s not found in executable image", pkgPath, typeName)
}

func Instantiate(pkgPath, typeString string, isPtr bool) (reflect.Value, error) {
res, err := searchForType(pkgPath, typeString)
if err != nil {
return reflect.Value{}, err
}
var emptyFace interface{}
ptr := (*eface)(unsafe.Pointer(&emptyFace))
ptr._type = res
ty := reflect.ValueOf(emptyFace).Type().Elem()
if isPtr {
e := reflect.New(ty)
return e, nil
}
e := reflect.New(ty).Elem()
return e, nil
}

对外就暴露个api即可。

1
2
// Instantiate returns an instance of a given full-package-path type string.
func Instantiate(typ string) (interface{}, reflect.Value, error)

测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
"fmt"
"testing"

"github.com/titanxxh/di/example"
_ "github.com/titanxxh/di/example/en"
)
func TestInstantiate(t *testing.T) {
{
a, _, err := Instantiate("github.com/titanxxh/di/example/en.english")
if err != nil {
panic(err)
}
fmt.Println(a.(example.Greeter).Hello())
}
}

然而并不能正确反射出来,报github.com/titanxxh/di/example/en, en.english not found in executable image,这就很奇怪了,我明明匿名引入了啊。

最后经过一番调查才发现,go会优化掉没使用的类型,我们需要搞个全局变量引用到自己的类型一下才行。。。比如这样。

1
2
3
4
5
6
var avoidOpt interface{}

func init() {
// use this symbol to avoid compiler optimization
avoidOpt = english{}
}

于是我们终于正确获得输出hello

再结合之前的plugin,就是我们的example啦。

项目地址在此,求个star呗。

压缩算法笔记

Know how to solve every problem that has been solved.

What I cannot create I do not understand.

–Richard P. Feynman

上个月因为公司内部的比赛,被迫短时间内了解了一些压缩算法,还动手实现了一些,比如lz77,deflate,bwt,bcm等,不实践不知道,一写代码就发现有些东西你以为你懂了实际上你没懂,加上最近看了已故物理学大师理查德费曼的一系列视频,其中一个细节让我印象深刻,他去世后大家在他办公室的黑板的左上角(这样就可以防止不小心被擦掉)发现他一直保留着上面的两句话。

理查德费曼很小的时候就受到他父亲的教育,明白了知道和理解是两个概念,所以他一直能保持好奇的心态去思考每个问题。
这两句话的本质是一样的,也就是——只有自己能做出来才算真正理解了,要做到这一点就需要知道每个问题背后是如何真正被解决的,而不是只知道个结论,所以理解一个概念的最高境界就是你能教会别人这个概念。

有感于此,特撰此文记录一下这段时间研究压缩算法的过程。

阅读更多...

GopherChina2020个人总结

如果你是新入坑的gopher那么建议一定要看一下Go Programming Patterns这个演讲。本次大会讲框架的比较多,听下来感觉go-zero做的比较完善,也更适合小公司或者个人上手,而且作者比较有激情。然后推荐那个Go编译器的、TiDB遇到的问题的、还有探探的。最水的是一个老外的Go in the Cloud - Why People Choose Go for Cloud Computing。。。

由于大会第二天分了两个会场,所以一个人只能听到部分的演讲,所以我在会场1和2反复横跳,挑着听了一些。比较遗憾的是阿里的那个EDAS的没听到,据说讲的比较好。

大会的PPT可以在这个git仓库找到

现场偶遇了前同事666,一开始带着口罩还没太确认,后来坐下来发微信才确认。

  1. 探探 ttdb
    探探是本次大会的联合主办方,是一个年轻人社交的app,呃,其实他们的现场的宣传片里面好像还有60多岁的使用者。如果让你设计一个系统,扫描两个人经过的地理位置,从而进行匹配出擦肩而过的人,你会怎么设计?
    他们介绍了自己的数据库的一些实践经验:

    • sql解析上的优化,比如做谓词下推等
    • 列存储,selection阶段利用cache coherence提升性能
    • 故障检测没有选择去中心化的gossip,而是使用的中心化的超时检测,故障恢复时候避免所有请求打到一个新的从节点导致峰值,而是偶尔将请求给从节点,保持热数据。
    • 利用这个库来进行goroutine的泄漏检测。
  2. Go Programming Patterns
    这个演讲里面的内容其实是我们平时代码里面都常会用到的一些小技巧还有一些控制反转等理念,尤其建议新接触Golang的同学都可以听一下,重点体会一下看开源代码学习好的编程模式的这种理念。另外,作者应该是比较熟悉各种语言的特性,所以对Golang也有很多吐槽。

    • functional option,这个应该应用是最为广泛的pattern了,几乎各大开源项目里面都用,很好地解决了可选参数必选参数区分,内部结构封装不破坏,使用时自注释等一系列问题。
    • go generation,因为不支持泛型(至少在2021年之前),所以很多时候利用好go generation是很必要的。
    • 错误处理,这里他提到rob pike的一个上古文章建议大家看看,但是我不确定他指的是哪一篇,不过go1.13新增加了errors.IsAs 两个用来帮助处理函数,同时fmt.Errorf里面支持通过%w来wrap一下error了。
    • kubernetes visitor、装饰器、pipeline等就看ppt吧,讲的比较清楚。
  3. Grab Food
    Grab也是GopherChina的老赞助商了,Grab是东南亚的超级巨无霸,可以理解为美团+支付宝+滴滴+高德地图。
    他们先是介绍了他们的Grab-Kit服务框架,和其他框架一样,支持各种middleware,validate、trace、throttling等,还支持chaos来模拟失败(捣乱猴?)
    然后介绍了Grab Food内部的ML Pipeline:

    1. 候选集合
    2. 过滤
    3. 重排序
    4. 后排序
      先引用Google的文章列了下当前机器学习的痛点,介绍了他们的chimera系统是如何做到持续集成、持续训练、持续交付的自动化的,做到了ML-ops。并提到了他们用了一种multi arm bandit AB test来解决模型的适应性问题。
  4. go-chassis
    这个是我司的人讲的,略过。

  5. Functional options and config for APIs
    这个演讲内容和前面第二个撞车了,看前面那个就行。

  6. 百度BFE
    这个老兄的因为主要不是什么技术型的演讲,所以我没怎么听就跑了,里面介绍了一些做技术管理的东西。

  7. go-zero
    又是一个搞框架的,不过这个老兄挺有意思,他说自己70后还在笔耕不辍的写代码。他这个go-zero是他们公司内部使用过程中迭代出来的东西,不是其他一些框架可能是脱离具体业务的产物。听下来感觉go-zero的功能还是很完善的,的确一站式解决了一些开发上的boilplate代码,而且他上来就是说我希望自己团队里面风格尽量统一,所以才有的这个东西。除了这个框架本身,我印象比较深的是他其中加入的一些自己产品中的解决问题方案。

    • 面向故障编程。
    • 不要join!
    • 如何正确的设计缓存,防止缓存穿透、击穿、雪崩。
    • 负载均衡上他提到了Power of Two Choices算法,这样既可以保证尽量多的就近选择,又动态控制不会把附近的服务处理时长搞得太长。基于这两篇文章12可以仔细研究下。
    • 自适应熔断,放弃了Netflix的Hystrix,介绍了他们用的Google SRE算法,支持自定义触发条件等。
    • 自适应降载。
  8. Go+
    由于布道师许式伟本人没来,找了个人临时讲,效果就不好,没有讲清楚为Go+替代Python搞数据科学的必要性,虽然堆了一堆PPT。。。Go+的运行可以使用转换成Go语言编译运行,也可以转换成字节码直接运行,性能差一点,感觉这个很鸡肋直接python不好么。而且当前不支持interface,没有runtime优化。

  9. PingCAP-Go runtime related problems in TiDB production environment
    这个演讲中提到的问题其实在我们工作中也遇到了,演讲者是golang internals的作者。
    第一个问题是golang的goroutine调度导致时延变大,其实这个我理解并不算是问题,或者说golang的goroutine调度实现就是如此,golang不像别的语言是原生支持了协程,同时也就绑定了他的实现,很多其他语言是通过库的方式来做的协程,调度器其实是可以根据需求来换实现的。所以如果要在go里面解决调度问题,要么就只能把高优先级的给绑核了。
    第三个问题是GC的bug
    第四个是NUMA的使用的建议,GC不是NUMA aware的,在allocation里面的帮助扫描触发之后,在众核环境下表现很差。这个现在版本已经优化了。

  10. Go语言编译器简介 史斌
    这个老兄就很硬核了,是给golang提交代码最多的50人之一,拥有Go的git库的提交权限,也年年收到go官方的邀请去美国开会。
    他提到了Go语言其实用的是自举,而不是走LLVM这种通用的方案,所以很多在LLVM那边做的优化,golang的编译器都没有,需要添加(大佬你的提交就是这样来的吧)。golang从源码到汇编有48道工序,他讲的话估计三天都不够。最后他选了前端、中端、后端的优化讲了一下自己的patch,还是挺有意思。

  11. GORM 剖析与最佳实践
    这个应该是国内做的最好的golang开源项目了,作者介绍了GORM2.0的重构和设计和一些最佳实践,感兴趣的可以仔细看下,当前暂时没用之后用了再回头看看。

  • Copyrights © 2011-2022 仙雾

请我喝杯咖啡吧~

支付宝
微信