过去几年的开发生涯,我一直都在思考 技术成长/核心竞争力 这些命题。
程序员这个行业面临的年龄危机和焦虑感是前所未见的,这些命题也将伴随程序员的整个职业生涯。在计算机软件工程的一些经历著作里面,前辈们也给出了不少关于这些方面的参考。但正如你无法只通过阅读理论而学会游泳一样,正如好的架构设计是慢慢地从解决问题和大量实践中生长出来的一样,真正的答案需要在大量的项目经验和编码实践中不断寻找。
我想,我的答案可能都藏在过程里面。作为一名非典型的前端开发,无论是在初期转做服务端开发工程师,还是负责开发亿级流量的前端监控接入层服务,又或是前端开发经历,虽说走了不少弯路,但都锻炼了我的技术能力,拓宽了我的技术视野,并且带给我很多解决问题的思维。
对于技术需要怎么具体地去学?项目需要怎么具体地去做?这些竞争力是不是能伴随我们度过 35 岁的门槛?希望我能通过这篇文章的复盘给出一些答案,同时能给各位读者一些启发。让我们一起共同面对这高悬头上的达摩克利斯之剑。
01 重视基础然后呢?
相信大家对于“程序员一定要重视基础知识”这句话已经烂熟于心了。但这句话有点高度概括,我时常在想,重视基础知识,然后呢?我们需要怎么学,并且怎么把学到的知识运用到实际的项目里面产生价值?
结合我自身的经历,我的观点是:保持好奇心。
保持好奇心去学习基础知识是富有成效的,因为拥有了好奇心,你的脑海中就会出现各种各样的问题,再以问题出发去思考它们是怎么被解决的,解决方案被采取的理由是什么,如果是我在面对条件 X 是否还会采取这样的方案?
回想起开发 TAM 前端监控接入层的时候,曾经遇到一个缓存类的问题。
问题的背景是前端监控接入层的数据设计导致需要提前将所有的项目数据加载到内存中去,而经过调研和实践,我基于本地缓存与中心化缓存,采用微服务中 refresh-ahead caching 的策略即提前将缓存加载到服务内存中,既满足缓存的需求又避免了中心化缓存的惊群效应问题。
常规的解决问题到这里已经结束了。但由于 2.0 的接入层是使用 golang 重构的,相对于 node.js 而言,在内存中读取数据是存在锁的。
1.1 保持好奇心地不断提问
那为什么 node.js 不需要锁,而 golang 需要锁呢?带着这个问题我继续深入研究,发现原因是 go 的内存模型导致的。所谓内存模型就是规定 golang 中不同的 goroutinue 在什么条件下保证可以读取到变量在另一个协程写进来的值。
深入研究之后发现其实是由于 CPU 的架构和内存重排的机制,导致不同协程之间需要通过锁(即内存屏障)来保证不同协程之间能准确读到写入值。
在对锁的问题有了这个认知以后,更进一步地思考与分析,内存缓存数据是读多写少的策略,能否通过将读写锁进行分离从而减少性能损耗呢?甚至更进一步,能否彻底使用无锁的策略,实现性能的提升呢?
带着这个疑问,调研发现类似 Docker 之所以能快速读写(建立 docker layer),是因为使用了 COW(Copy On Write)写时复制的策略,从而达到无锁机制以提升读写性能。最后我也是实现了类似的策略,将内存缓存数据的读取速度比原来提升了至少 500W 次以上。
在优化的过程中,我最大的感受是保持好奇心能够驱使我去了解某项技术的原理。
1.2 什么是基础知识中最重要的东西
但我觉得其实最重要的并不是原理本身,而是借由现实遇到的开发场景下,去窥探前辈们在优化系统时,会采取什么样的思维和思路来分析与实践,比如上面所说的缓存策略,本质上和 CPU 的多级缓存在思路上是类似的,CPU 的一级缓存是单核内的,如果想要多核同步,需要借助三级缓存,一级缓存类比服务内存缓存,三级缓存类比中心化的 Redis 缓存。
带着这样的思维,我来到了文档团队,很快就遇到了关于网关的工程化建设问题导致间接依赖的锁版本问题,最终是通过 pnpm、pnpm hook 和 docker context 来解决的。
在解决过程中,我同样对于 pnpm 本身为什么能够节省磁盘空间有了好奇心。研究之后发现,其实是和软硬链接有关。
研究之后发现软链接其实是一个指针,而硬链接是一个完整的 inode 文件,那说明在执行逻辑的时候,软链接是会按照原文件的位置进行依赖查找,这样即便项目的层级发生了变化,使用软链接可以使得服务启动的位置和脚本并不需要变动,这样就极大地降低了验证成本,并且更重要的是,团队以往的运维经验(查看日志/配置等等)是可以继续复用的,这也是无形中降低了因改造升级带来的心智负担。
解决问题本身和学习基础知识会带来巨大的成就感和内啡肽,但最大的收获我个人认为是通过深入研究获得了解决某一类问题的思维,这才是最难得的点。
02 重视输出
如果你的目标是这场一定要炸,
那准备就永远是徒劳的。
如果你的目标是变成一个
越来越强的脱口秀演员,
那就请永远都好好准备。
《李诞脱口秀工作手册》里面这样说到。我觉得对于工程师来说,也是一样的。
在日常的开发工作里面,需要注重对于开发经验的积累与深入了解技术细节的思考与想法,哪怕这些想法可能对于熟悉项目的开发来说是已知的。
我个人的观点和上面的引用类似:我并不预设写这篇文章会“炸”,会受到很多的点赞,而是我努力成为一个越来越靠谱,解决问题的思路越来越清晰的程序员,因此我需要写文章来总结我的思考想法与这段经历里面学到的东西,毕竟写作和输出这件事情更多的还是为自己而写。
2.1 在整理信息中获得新的灵感
一直以来,我都是按照这个路径来慢慢努力。记得刚来到文档团队的时候,接手了持续部署 CD 发布系统相关的事情。CD 发布系统刚开始的时候相关的说明文档是比较少的,更多时候需要自己去看具体的逻辑,并且整个链路涉及了比较多的异构系统,遇到某个问题需要定位的时候往往需要耗费很多时间在不同的系统里面与各种细节做斗争。
在理顺各个子系统之间的逻辑之后,我开始慢慢沉淀相关的 oncall 文档和串讲文档共 27+ 篇。当时的想法很简单:“我想要把我的运维经验进行复制,从而减轻自己和团队的运维压力,沉淀文档无疑是目前比较好的方式”。
很多人是不愿意做类似这样的工作的:
代码腐化+文档缺失 会极大地增加认知负担,使得某些功能的流程难以辨认,不知道从何下手。
应对方法也很直接,要做的就是代码防腐以及知识沉淀,但这些恰好又是很多人嫌麻烦不愿做的地方。
毕竟人都是自私的,谁愿意干前人栽树后人乘凉的事儿呢,多堆点需求帮业务挣钱拿个五星去晋升不香吗,我为啥要防腐为啥要写文档…
确实,我认可文中的观点,写文档这件事情很多时候看起来是“很笨”的,但我觉得这是应该做的并且是正确的事情,记得罗永浩在《我的奋斗》里面有写到:
如果你一生耿直,刚正不阿,没做任何恶心的事情,没有做任何对别人造成伤害的事情,
一辈子拼了老命勉强把老婆、孩子、老娘,把身边的这些人照顾好了,
没有成名,没有发财,没有成就伟大的事业,一生正直,最后梗着脖子到了七八十岁死掉了,
你这一生是不是没有改变世界?
你还是改变世界了的,你把这个世界变得美好了一点点。因为你,这个世界又多了一个好人。
整理信息对个人,项目,团队可能都往好的方面更进了一步,即便都没有,至少也对这个项目更熟悉了一点,这本身也是工作的一部分。
况我也确实从整理信息积累文档中逐渐获益。在整理 oncall 文档中发现,平时更多在处理的事情其实是「琐事」,根据《Google SRE 运维与解密》一书里面的定义,所谓琐事:
1. 手动性
2. 重复性
3. 可以被自动化的
4. 战术性的,琐事是突然出现的,应对性的工作,而非策略驱动或者主动安排的。处理紧急告警是琐事,我们无法完全避免它,但是可以尽量减少它。
5. 没有持久价值的。
6. 与服务同步线性增长的,服务任务和服务的大小,流量与用户数量是线性增长的,那么这个任务就是琐事,一个良好管理和设计的服务应该至少可以应对一个数量级的增长。
工程师需要去能减少这些琐事,针对这种情况,我们需要工程工作来进行创新性地创造性地来解决。
在整理并文档的过程中,我思考这些功能当时为什么要这么设计?有没有更好的设计方法?哪些形式是好的需要延续的?哪些形式是对目前团队没有帮助需要去掉的?过程中也和团队的人讨论过,其中有个问题我觉得还挺有意思的:
我们到底是 gitops 还是 chatops ?为什么有两种模式?目前的瓶颈是什么?
这个问题不仅牵扯了技术层面的建设,也牵扯到团队协同方面的事情。文档团队横跨北京,深圳,武汉等多地,怎么管理与协同团队之间的发布节奏和配合?技术上怎么帮助这个事情落地?简单的 gitops 能不能满足逐步灰度的策略和协同发布节奏?
最后根据整理的文档和思考下来的结论是,研发流程是根据团队管理和模块划分的基础建立起来的,两种模式其实都是需要的,而问题的症结点是在于基础模块的性能导致流程反馈比较慢,因此我们获得了后面的优化方向。
更进一步,结合目前市面上开源的优秀工具,吸取他们的思想,我们逐步形成了我们自己的 CD 发布系统的新方案:
更多地减少异构系统,利用整体的设计来规范与改进流程的可观测性,计划使用轻量的模块来提升性能。
*PS:方案正在投入开发中*
最后,我觉得即便输出了的文档也无法沉淀出新的方案,又或者是新方案无法完美匹配多样多变的发布节奏和策略,但至少在这个过程中,我锻炼了自己在复杂系统中提炼精简信息的能力,这也许也够了。
2.2 减少 Tacit Knowledge
而除了能够在写作中获得新灵感,另外一个重要的点在于减少程序员的 Tacit Knowledge,也就是所谓的内隐知识。
在工程代码里面的设计和决策,可能都有过激烈的讨论和渐进式的尝试和思考。但是如果不沉淀输出,它就只存在最开始的开发者脑中。
它只存在开始那个开发者脑中,随着那个人的遗忘或者离职,这些内隐知识将永久丢失。所以通过文档沉淀内隐知识对于项目是非常重要的。——「理解业务系统的复杂性」
这些文档缺失会增加后来者维护时极大的心智负担。
对于我来说,我往往会选择记录下来,想法也比较简单:我想梳理一下看看自己的想法有没有问题?沉淀出文档让大家一起讨论看是否有更优的解决方案。
于是,我在优化 web-gateway 的工程化的时候,将自己优化过程中的思考和试错都记录下来,方便自己和大家都审视一下这些方向。
这样不管是维护的同学,还是想优化或了解项目的开发,都可以通过文档来知道为什么我们选择使用 pnpm 来管理模块而非多仓管理,为什么我们在打包 Docker 镜像的时候使用 pnpmcontext 脚本作为上下文,而不是简单的使用本地路径即可。
03 永远都选 A
讲完了上面技术向的东西,我还想和大家分享一个在生活中的观点(鸡汤):永远都选 A。
纠结、焦虑、恐惧怎么办?
A: 放手去做
B: 再等等,再准备准备(划掉)
其实遇到问题,大家都会有恐惧等等一些情绪,这是很正常的。因为这是刻在人类 DNA 里面的,即便经过亿万年的进化,面对未知,我们有时候还是会退化成那个有惯性的原始人。
以我自己举例,记得在实习入职第一天,导师因为我在校期间写过 Koa 相关的项目,分配我担任 Node.js 服务端开发工程师。
但是第二天给我分配的任务是:“排查一下业务系统高并发下偶现 502 问题”,但是我当时并没有太扎实的服务端的开发经验与知识,虽然最后依靠逐步的排查和学习解决了问题,但现在回想起来,当时花了太多的时间在应对恐惧焦虑情绪上,在思考“我是不是应该重头多学学基础知识再来解决”,而不是专注在解决问题上面。
3.1 你是在预热还是在预退
遇到问题,把自己的选化为单选也就是直面问题解决,有时候反而最大化地能去除不必要的精神准备和没必要的挣扎。生活中可能时不时就出一个升级的问题,需要我们去着手解决,其实问题出现之后,应该是着重精力去思考怎么解决问题,不需要过度准备,不需要“重头学习再来解决”,在做中学或许才是答案。
又或者,我们应该换个心态来看待这些问题:“我走在成长的路上,变强,所以一个个升级的问题都找上门来了,这是正常的”,只要去积极应对就好了。
其实对于我自己,我也没有完全做到遇到任何问题都:永远都选 A。但是我想在接下来的生活里面尝试与践行一下,毕竟成长就是克服天性的过程,与大家共勉。
04 最后
文章的最后,上面这些都是我比较浅薄和简陋的看法,这些并不完全正确,我也会和大家一起,在时代的变化中不断修正自己的看法。
感谢大家能看完这篇文章,如果读完能够对大家有那么一丁点的启发,那就足够了,希望大家都能在自己的开发生涯中不断打怪升级。
最后,也感谢 leader 们给的机会让我有机会在更多更大的平台上学习到更多,同时也少不了身边同事朋友的帮助,感谢大家!