如何让地球离了你就停转

最近张小平先生离职引发登月工程延期的事情传遍了很多媒体。我对此事没有足够的信息,不便判断。但从程序员这个行业看,因为心累而离职是top3的理由。反之,程序员要如何做才能确保地球离了自己就停转才是本文讨论的主题。

让地球停转的根本在于开发的系统要有足够的部署,足够的上下游关联,缺乏文档且难于跟踪调试。所以如下分几个方面讨论。


部署量

一个程序员开发的系统如果没有用,未被大规模部署,自然是可以被随意丢弃的。所以第一步就是要争取让自己的项目可以被部署的到处都是。在一个团体里,是要努力争取才能让自己负责某个系统或某个模块的。所以工作中无论何种方式,多争取负责的项目就成了关键。自己能不能干的完是另外的概念,先抢项目要紧。

这一点在国内的大型互联网公司里是比较常见的。一个程序员在一个大公司里工作超过2年的,几乎必然拥有不止一套系统在运行中。不管这个系统有多烂,多么频繁的出问题,只要还在运行还能对付着用通常就不会被替换。

这种拥有了一个运行中系统的程序员有相当一部分会开始选择在大公司里混日子,这是大公司里效率低下的一个原因。只要不停的向上汇报说这个系统如此重要,但需要不断的投入自己的人力去维护其运行。包括但不限于:清理脏数据、日志备份与分析、低频需求的手工处理、Bug的跟踪与修复、硬件损坏后搭建新的环境、重构、性能优化、构建测试用例、补文档。但实际情况就是不断的摸鱼,反正这些工作都很难量化,做了跟没做一个样

为了系统可以正常运行,很多Leader即便一眼能看出摸鱼,也会选择沉默,毕竟运行中系统出了问题是很难堪的。同时其他人也在摸鱼导致无法抽出足够的人力去替换这个系统。

上下游关联

除了作为独立系统,更好的选择是让自己的系统成为分布式系统中的一环。从上游系统接受一些东西,再吐给下游系统。在这样的环境里,就会使得自己与上下游系统的接口兼容变得非常重要。

当有人胆敢替换你的系统,他就会面临接口兼容的难题。上游过来的数据结构,每个字段的含义与依赖,字段值的取值范围,每个字段与数据库中已有内容的对应关系和计算关系。对下游系统也是如此,尤其是几层嵌套的数据结构,跟踪起来难度就会增大不少。尤其是某些字段值再经过几层计算后再输出,跟踪难度就会剧增。

文档

好的文档自然是项目交接的必要条件。但对常见的软件系统,并不是文档很多就是足够用的。有些关键文档,字数也许不多,却是非常重要的。自己开发系统时,将这些文档作为离线文档,或者干脆不写就会极大的增加地球自转的阻力。

建模文档。是最常见的重要而几乎没人写的文档。用以描述一个系统中各个部分的关系,以及工作流程。典型如libav库,作为一个音视频处理的库,只有API文档,却没有说这些API的调用先后顺序,以及不同任务需要调用哪些API。只有光突突的API文档导致了libav的应用开发十分恶劣。nginx扩展模块的开发也是如此,甚至连API文档都很少,更别说建模文档。

数据库设计文档。在大部分公司已经不允许使用数据库高级特性的环境下,比如外键约束。导致了数据库之间的关联就只能靠文档来解决。此时若外键字段的命名设计的没有啥规律,加之若干缩写,就会使得后续开发人员几乎无法有效的识别出这些约束。重构或重写过程也会频繁碰壁。

API文档。是确保上下游系统对接成功的关键,也是制造混乱的关键。但完全没有API文档又有那么点不像话,所以可以尝试只写少量而混乱的。比如有50个API,就只写其中4个API的文档。需要上下游系统对接时,使用离线文档发给相关的开发人员,反正离线文档很容易丢失。

代码混淆

代码混淆是避免其他开发人员接手的比较底层的手段,通常若其他开发人员走到这一步就已经比较坚毅了。

代码中要善用缩写,并混合使用英文缩写、拼音缩写、法语词缀、西班牙语的逗号句号。字符方面只要Unicode里存在的都不要吝惜,毕竟现代大部分编译器已经支持了这些标识符。如果不愿意查Unicode表,也可以善用l、1、I,或者O、0等。

对这些还不熟悉的,可以考虑网上找代码混淆工具,可以批量生成恶心人的代码。

一些特殊的编程风格也可以成为代码混淆的利器。比如我就见过用函数式编程风格写的一整行Python代码。

运行时配置

对于程序的配置,在配置文件里的比较容易就能分析和理解。反之运行时配置的内容,因为都在内存里,就基本没法玩了。典型的可以给自己的程序提供个配置接口,使得外部可以通过接口调用发送配置过来,并实时修改配置内容。

更简陋点的,可以在程序启动时,通过stdin输入一些必要的配置,否则就使用默认而无法使用的配置。

运行时配置其实已经是一些系统的功能了,所以给自己的系统加上了也没啥不妥。典型如Linux防火墙iptables。都是通过命令配置的,配置内容存储在内存里并立即生效。只要重启就无法复现。

运行时配置确保了,离了你再也没人能部署出一个新的运行环境。而且当前环境一旦重启也就立即无法使用了。

不确定的外部依赖

这是在软件的构建阶段所做的工作。使得自己的开发环境在其他工程师手上根本无法有效的搭建。在C/C++里很常见的情况是一些第三方库的大版本升级是接口不兼容的升级。而构建环境通常只是指定路径却没有指定用了第三方库的哪个版本。所以当有人尝试接手你的系统时,如果依赖十几个第三方库,挨个尝试版本的组合就足以严重打击一个人的自信。

不过还有更优秀的nodejs和golang。这两种语言是我所见在外部依赖方面做的最优秀的。其依赖库使用github上自动拉回最新的代码。连个版本号都没有,而且这些第三方库的作者也经常会修改API导致不兼容。哪怕几天的两个程序员都没法搭建出一样的开发环境。相隔一个月以上就会有较大的概率遇到接口不兼容升级。甚至作者可以耍一点小心眼,把自己的部分代码放在自己申请的github帐号里,作为第三方库引用。离职了就把自己github帐号删了。

使用操作系统、编译器特性

Linux系统和gcc系列编译器有很多可爱的特性,使得你可以写出移植性极差的程序。

典型的如Linux系统的/dev/shm目录,就是个内存盘,可以尝试把程序依赖的一些内容放在里面。自然是重启就会失效。

Linux的/tmp目录也是个不错的选择。很多发行版会在重启时自动清空/tmp下的内容,大部分发行版也会每隔几天自动删除/tmp下的内容。所以可以尝试把启动配置文件放在/tmp里,反正几天后就神奇消失了。PID文件也可以考虑放/tmp下,导致后续启动的多进程程序谁都分不清哪个是主进程/控制进程。

gcc方面则可以善用内联汇编,引用哪些CPU相关的高级指令集做一些计算。比如SSE、AVX系列等等。甚至可以通过内联汇编判断当前CPU缺少某些高级指令集时就自动crash。这样换一台机器程序就跑挂了。

另外像结构体里的字段对齐也是个不错的选择。32bit系统会自动把字段值按照32bit对齐,使得结构体中间会有一些内容随机的空洞,可以定义两个结构体指向同一地址空间后善用这些空洞来存储一些小秘密。

时间依赖特性

这个技巧就比较炫酷了。比如执行某个操作时,可以启动个线程去做,并立即返回,之后sleep个一定时间,或用其他操作来模拟等待,比如读写文件。再之后依赖线程执行结果。这样可以确保程序被复制到性能更高或更低的机器上时都会出问题。

粗糙一点的玩法可以提取当前系统时间来判断程序什么时候可以死。Linux系统也有系统启动到现在的时间,可以用于控制程序在运行一段时间后就挂了。

时间依赖的玩法可以很多,而且调试难度很高,是制造恶心系统的优秀方法。

内存溢出

内存溢出不仅可以确保程序无法运行太久,也可以在自己仍然在职的时候,给自己制造工作量来不断的做程序的清理工作。方法有很多种,对C/C++这类没有自动GC的当然好办。

对有自动GC的语言,可以使用一些全局变量,或可以长时间存在的变量来存储一些内容非常复杂且持续增长的数据结构。

不定时的Boom

此方法的核心是不断制造会占用资源的垃圾,类似于内存溢出,使得软件运行一段时间后就会因为到达资源限制而出错。典型的如不断写入的日志最终会耗光硬盘空间。速度更快点的可以开个内存盘来写。

操作系统同时打开的Socket数量也是个不错的选择,毕竟很久前Linux默认设置也就是开几千个链接。

数据库里也有很多办法。比如对一些频繁发生的事务记录到数据库,美其名曰易于跟踪调试。典型如HTTP请求,同时自增的ID却用了32bit的整数

总结

如上所有内容并非我临时起意的编造,而是过往工作中,接手一些老系统时,前任开发者坑我的方式。一步步坚持了过来,得以积累的内容。

一个程序员如果选择了使用如上这些手段来保住自己的职位,自然可以开心的摸鱼,但也会逐渐毁了自己的职业生涯,甚至会跟一些不大的公司同归于尽。建议慎用。

发布于 2018-09-27