一个程序员的进阶之路:从毫无经验的应届生到一战封神的高级软件工程师,我经历了什么?

一个应届毕业生如何从没有任何实战经验成长为可以独当一面的工程师?

写了几年的业务逻辑,如何打破天花板,摘掉“CRUD(增删改查)Boy”标签,成长为更高级的软件开发工程师?

……

相信不少小伙伴在自己职业生涯中都有类似的困惑,本文源自《程序员进阶之路:缓存、网络、内存与案例》一书作者邓中华老师的真实经历,希望可以给各位追求成长的小伙伴一些启发和鼓励!

入行游戏业

记得毕业那年,找了好一段时间的工作,公司都想要一上来就能干活的人,然而对于我一个刚毕业的学生来说根本做不到。最后终于有一家创业的游戏公司招聘了我,愿意培养应届生,做的是一款FPS微端游戏,可能很多人不了解什么是微端,简单讲就是轻量级的端游,有点像页游但不需要浏览器,后来随着手游的风口,微端改为了手游,我的工作是游戏服务端开发。

01

如饥似渴

刚开始工作时,心里很着急,想要尽快学习工作中用到的知识,尽快完成身份的转换,我想这是很多毕业生的共性。想要一口气看完整个服务端的代码,但是看完这块忘了那块,根本联系不起来,就好像脑袋中的内存不够一样。

那会儿我的导师(G)跟我说:别太着急,一点点来,慢慢就都了解了,一个月业务逻辑就差不多弄明白了,3个月就可以独当一面。

说实话当时我是不自信的,怀疑他说的话在我身上是否成立。

随着导师的耐心指导,再加上自己潜心学习,3个月后,自己真的可以独挡一面,自己接需求,并完成开发任务。

这里要提一点,刚毕业遇到一位好的导师是很重要的一件事情,我非常感谢他对我的帮助。

第一任导师会影响整个后面的工作心态,所以我也会善待我带的应届生。

我真的见过一个没被善待的应届生是如何不善待他带的应届生的。

但是话说回来,导师是分配的,对于刚毕业的应届生来说没得选,靠的是缘分。这里也呼吁一下带应届生的同行,请善待他们,别给他们的整个职业生涯留下心里阴影,我们要传递善意。

02

一夜暴富

每个刚进入游戏行业的朋友多多少少都是奔着游戏爆火、一夜暴富的目的。

但现实却很残酷。

我从事了6年游戏行业,一个游戏都没有大卖,所以说不要抱有任何幻想,成功的概率太低了,这也是后面我转战互联网的一个原因,有点心灰意冷了。

但是还真有特例,我的一个同事(他是被我内推到那个项目的)做的游戏还真的大卖了,每个月发的奖金比工资都多,后来在北京买了房,实属羡慕。

03

业务主力

从事了6年游戏行业后,一直在写业务逻辑,基本上在性能优化等方面没有任何建树,并且逐渐觉得开发业务逻辑没有意思,仅仅是体力劳动罢了,没有挑战,也没有成长空间了,认识到写5年和10年业务逻辑在个人成长方面没有什么差别,只不过业务逻辑更熟练罢了。因此开始琢磨转战互联网,想要从事高并发和更接近底层的工作,因为业务逻辑真的写吐了。

有人可能会问,为啥不在游戏行业从事这些工作呢,因为游戏行业注重开发进度,大量堆砌业务逻辑,开发节奏非常快,哪里会给你优化和从事底层的机会,即使有,需要的人也是少数,轮不到你。

你见过有2万行代码的任务系统吗?我就见过,而且开发、重构和维护过,真的是牵一发动全身!

你要相信从游戏行业出来的人的逻辑开发能力,他们都是经过了具有天马行空想法的策划同仁的考验,才会站在你面前。以至于后来从事互联网业务开发,觉得互联网的业务也太简单了,都不费什么脑力。

转行互联网

下定决心转行做互联网服务端后,我面试了很多互联网公司,人家都说我做的是业务开发,没有做过什么有挑战的工作,例如内存泄露问题和性能优化问题等工作,将我拒之门外。即使拿到Offer了,很多公司也压职级和薪资。后来遇到了一个互联网大厂,面试效果比较好,愿意接受我的转行。

这里有些仅从事过游戏行业或者互联网行业的朋友可能不清楚为啥游戏到互联网需要转行,不都是写代码吗。

还真不是写代码那么简单的事,需要思维上的转变。

游戏更注重复杂业务逻辑的开发,每组服务器,也就是一个区,玩家数量是比较少的,一个用户频繁与服务端通信,服务端性能不够那就多开几个区;而互联网的服务器注重性能,基本没有分区的概念,用户不需要选择登录哪个区,只不过有集群的概念,服务的用户数量非常多,每个用户与服务端通信却没那么频繁。有的互联网公司的面试官会觉得游戏服务器承载这么点用户量没有技术含量,这就说明隔行如隔山。

01

如鱼得水

因为本人喜欢做有挑战的工作,加上前面提到6年业务开发早已厌倦,非常想要做性能优化等偏离业务本身的底层工作。来到新的公司后,领导(L)给机会研究网络库和状态机等核心C/C++底层库,我非常感谢这位领导。这段时间我非常开心,终于如偿所愿。积累的游戏业务经验没有用了,不过经过之前的业务逻辑的锻炼,互联网业务逻辑手拿把掐,但是需要补齐互联网的工作流程和思维。

新公司的第一个任务是排查一个C++服务的内存泄露问题,这个问题已经困扰他们2周了。说实话我是没有信心的,因为之前从来没有做过这类事情,但是我的内心又是欣喜的,我喜欢底层的、偏离业务的和有挑战的工作。虽然我没有排查过内存泄露问题,但是我可以研究啊,哪里不会就学习哪里,武装自己的技能背包。

1. 背景

我原来有一个使用C++编写的业务服务,迟迟不敢上线,原因是内存泄漏问题一直解决不了。具体现象是,该服务上线后,内存每隔几秒上涨 4/8KB,该服务不停止运行,内存就一直上涨。

2. 分析过程

使用Valgrind工具多次运行该服务,在结果中没有发现内存泄漏,但是发现有很多没有被释放的内存和疑似内存泄漏。

既然使用工具分析失败,那么就逐个模块查看代码,并且编写Demo逐个验证该模块是否有内存泄漏(使用Valgrind工具检测内存泄漏),很遗憾,最后还是没有找到内存泄漏。

这个时候两周过去了,领导说:“找不到内存泄漏那就先去做别的任务吧”,感觉到一丝凉意,我说:“再给我点时间,快找到了”。这样顶着巨大压力加班加点,对比多次数据结果,第一次Valgrind运行10分钟,第二次Valgrind运行20分钟,查看有哪些差异或异常,寻找蛛丝马迹。遗憾的是,还是没有发现内存泄漏在哪里。

功夫不负有心人,查看了N份Valgrind的运行结果后,对一个队列产生了疑问,它为什么这么长?队列长度为1000万个元素,直觉告诉我,这里不正常。

在代码中查找这个队列,发现在初始化队列的时候将队列设置为1000万个元素,这个长度值太大了。

3. 定位

新创建(new或者malloc)的对象在入队列时,因为访问了虚拟内存地址,所以需要把虚拟地址映射到物理地址,因此物理内存就会增加,但是当对象出队列并调用释放内存函数(delete或者free)时,物理内存不会立刻回收和取消虚拟内存到物理内存的映射关系,而是保留给程序一段时间(当系统内存紧张时会主动回收),目的是让程序再次使用之前的虚拟地址和其映射到的物理内存,避免内存申请和缺页中断的开销。

当服务启动时,程序在这1000万个元素的队列上一直不停地进/出队列,有点像貔貅,只进不出,自然会导致物理内存一直上涨,直到貔貅跑到了队尾,物理内存才会达到顶峰,物理内存的增长和回收开始处在一个平衡点。

在图1中,红色部分代表程序占用的有对应物理内存的虚拟内存,绿色部分为没有对应物理内存的虚拟内存。

图片

然而每次服务上线还没等到达物理内存增长和回收的平衡点就下线了,担心服务内存一直增加,为了避免出事故就停止运行服务了。解决办法就是把队列长度调小,最后队列长度调整为2万个元素,再上线,貔貅很快跑到了队尾,达到了平衡点,内存就不再增加了。

4. 总结

其实,本来就没有内存泄漏,这就是伪内存泄漏。一直不敢上线的服务终于可以正式上线了。

05

渐入佳境

经过上面的任务,得到了领导的认可,领导说我技术功底扎实(心虚,现学现卖),信心一下子就来了。后来的一件事直接使我“一战封神”,也凭借这件事情完成了一次升职加薪,事情的经过如下。

1. 背景

我们有一个业务,从2019年到2020年发生了四次大流量事故,发生事故时网络流量理论峰值为3Tbps(经过模糊处理),导致网络运营商封禁入口IP地址,每次事故造成几百万元的经济损失。

这四次事故均没有找到具体原因,一开始怀疑是服务器受到网络攻击,后来随着事故发生次数的增加,发现事故的发生时间具有一定的规律,越发感觉不像是被攻击,而是业务服务本身的流量瞬间增多导致的。服务指标都是事故造成的结果,很难倒推出事故原因。

2. 分析过程(大胆假设)
(1)发现事故大概每50天发生一次

记得2020年7月15日那天巡检服务时,我把snmp.xxx.InErrors指标拉到一年的跨度,如图2所示(经过模糊处理)。多个尖刺的间距似乎相等,然后我就记录了各个尖刺时间点,计算出各个尖刺间的间隔并记录在表1中。着实吓了一跳,大概是每50天发生一次事故,并且我预测8月18日可能还会发生一次事故。

图片

表1

图片

(2)联想50天与uint溢出有关

7月15日下班的路上,我脑海里在想:3600(一个小时的秒数),86400(一天的秒数),50天,5×8等于40,感觉和42亿有关系,那就是uint(  ),怎么才能等于42亿呢?86400×50×1000是40多亿,这不巧了嘛!我计算了三个数:

   = 4294967296 

3600 × 24 × 49 × 1000 = 4233600000 

3600 × 24 × 50 × 1000 = 4320000000

  在后面的两个结果之间,4294967296毫秒就是49天16小时多一些,验证了大概每50天发生一次事故的猜想,如图3所示。

图片

3. 定位(小心求证)
(1)查看代码中与时间相关的函数

下面的代码在64位系统上没有问题,但是在32位系统上会发生溢出截断,导致返回的时间是跳变的,不连续。

uint64_t now_ms() {    struct timeval t;    gettimeofday(&t, NULL);    return t.tv_sec * 1000  +  t.tv_usec / 1000;}

图4是该函数随时间输出的折线图,理想情况下是一条向上的蓝色直线,但是在32位系统上,结果却是跳变的红线。

图片

图4

这里解释一下,问题出在了t.tv_sec*1000上,在32位系统上会发生溢出,高于32位的部分被截断,数据丢失。不幸的是,笔者的客户端有一部分是32位系统的。

(2)找到出问题的逻辑

继续追踪使用上面函数的逻辑,发现一处问题,客户端和服务端的长连接需要发送Ping保活,下一次发送Ping的时间等于上一次发送Ping的时间加上30秒,代码简写如下:

next_ping = now_ms() + 30000;

客户端主循环会不断判断当前时间是否大于next_ping,当当前时间大于next_ping时发送Ping保活,代码简写如下:​​​​​​​

if (now_ms() > next_ping) {    send_ping();    next_ping = now_ms() + 30000;}

那么怎么就出现大量流量到达服务端呢?举个例子,如图4所示,假如当前时间是6月29日20:14:00(20:14:26,now_ms函数返回 0),now_ms函数的返回值超级大。

那么next_ping等于now_ms函数的返回值加上 30000(30s),结果会发生uint64溢出,反而变得很小,这就导致在接下来的26秒内,now_ms函数的返回值一直大于next_ping,客户端就会不停发送Ping包,产生了大量流量并到达服务端。

(3)客户端实际验证

找到一个有问题的客户端设备,把它本地时间改为6月29日20:13:00,让其自然跨过20:14:26,发现客户端本地log中有大量发送Ping包的日志,10秒内发送了2万多个包。证实事故原因就是这个函数造成的。解决办法是对now_ms函数做如下修改:​​​​​​​

uint64_t now_ms() {    struct timeval t;    gettimeofday(&t, NULL);    return (uint64_t)t.tv_sec * 1000  +  t.tv_usec / 1000;}
(4)精准预测后面事故的时间点

因为客户端升级周期比较长,需要做好下次事故预案,及时处理事故,所以预测了后面多次事故。表2的最后三行是当时预测事故时间点,并且随着时间的推移都逐一得到了证实。

表2

图片

4. 总结

解决该事故的难点在于大部分服务端的指标都是事故导致的结果,并且大量流量还没有到达业务服务就被网络运营商封禁了IP地址;并且事故周期跨度大,50天发生一次,难以发现规律。

发现规律是第一步,重点是能把50天和uint32的最大值联系起来,这一步是解决该问题的关键。

  • 大胆假设:客户端和服务端的代码中与时间相关的函数有问题。

  • 小心求证:找到有问题的函数,编写代码进行验证,最后通过复现来定位问题。

经过不懈努力,从没有头绪到逐渐缩小排查范围,最后定位和解决问题。

尝试自媒体

我一直想自己设计一个无锁的多生产者、多消费者队列。无锁编程涉及很多缓存和内存屏障的知识,不是一件容易的事情,没有技术功底是不行的。所以我查了很多资料,深入学习了很多理论知识,弄清楚每个细节,刨根问底,追本溯源,从CAS、内存屏障、缓存一致性协议,再到缓存原理,甚至底层硬件,终于弄明白了无锁编程的底层实现。然后把学习到的知识汇总整理成了《CPU缓存一致性:从理论到实战》——这是我在网上发布的第一篇技术文章。

01

公众号&知乎

我将这篇文章发布到公众号和知乎上后,获得了非常多的点赞,我也收获了很多粉丝。就这样,我发现了自己的另一面—除了可以自己写代码,还可以教别人写代码。

读者的认可激发了我想要创作更多的技术文章的热情。所以我后来又写了网络、TCP、UDP、端口、分布式和相关工作经历等的文章。 

我深知仅有一颗上进的心是不够的,还是得有人教和有人带才能成长得更快,仅靠自己的摸索很难走出困境。虽然码龄一年一年地增长,但是层次可能一直停留在初级阶段而不自知。

02

程序员进阶之路

在我工作满十周年之际,借此契机,我将这些文章整理为“十年码农内功”系列,例如其中的“十年码农内功:缓存”。后面我又写了网络收发包详细过程和内存等文章,填补了“十年码农内功”系列的最后几块拼图。再后来有几个出版社联系了我,想要将“十年码农内功”系列文章出版为图书,最后我选择了最早与我联系的电子工业出版社。将网络文章变成正式的出版物可不是一件容易的事情,在此期间我做了大量的修改工作来完善本书的内容,最终出版——《程序员进阶之路:缓存、网络、内存与案例》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值