刚毕业面试的时候,总有面经告诉你,参与开源代码是多么多么贴金的体验,能极大提高拿到 offer 的概率。当时,一直觉得是因为开源代码非常优秀,能参与是很了不起的人。直到,我自己实际来维护开源项目。我才知道,这个世界的水平就是那么参差的,好代码从来不是一个产品能不能赚钱的决定因素,甚至不一定是重要的因素。
一份代码写完之后,会有不同的使用方关注不同的部分:
-
开发者:需要长期维护,关注代码更细节的东西,比如命名、配置、日志、测试、模块化
-
使用方:在意错误码、协议和插件,好用就行
-
SRE:关心监控 和告警,确保系统不会出事
-
机器:负责运行代码,关注性能。性能是实打实的需求,不属于半点没用的范畴
命名
代码的命名就变量、函数。很多很多书籍都说过这块的内容。总结起来就一个点,能反馈它的实际作用。从这点来说,写代码和写一篇文章是一样的,你得上文提到过这个东西,给这个东西明确定义,下文才能引用。如果名字不清晰,就等于说根本没有给它一个明确的定义
比如,在 Get** 函数里面去更新一个啥的。这就不能准确地反映这个东西的作用。因为,我们阅读代码的时候,并不一定会一行一行去看,而是会先看个大概,特别是开源代码,动不动几十万行。
如果遇到函数里面功能比较多的情况,就考虑
-
要拆吗 ? 如果能功能分开,就尽量分开。比如一个函数既要 Get 又要 Update,那就可以拆成两个。如果拆完以后发现它两经常一起用,就再组合一个
-
拆不开就改函数名。我也会经常想一个函数叫 GetAndUpdate** 是不是太长了。但是,我研究生导师告诉我,内容决定形式,而不是形式决定内容。它就得这么长才能反映它的内容。而且,在比较优秀的开源代码中,也会用这种比较长的名字,它不会因为它很长,就读不懂
配置
为什么需要配置 ?基本上可以认为是低成本修改代码逻辑。这里的底层本就包括
-
逻辑生效的成本。代码修改的话,需要走一整套上线逻辑,没准还要等火车。动态配置能够及时修改就生效
-
修改的心理负担。配置相当于把一个逻辑从大量代码中抽取出来。无论是修改还是 review,心里负担都会减小
什么时候需要配置 ?一方面使用者希望有灵活的配置可以调整,另一方面我看到过配置超级多的项目,多到你都希望它不要提供这么多配置,给你看 捂脸。一般提供配置的地方,都是设计者觉得,可能会变的。
配置的方式,有很多,我接触过的,起码有:环境变量、文件、数据库、配置平台(比如 tcc)
具体用哪一种基本取决于
-
安全。很久以前秘钥都是写在配置文件里的,因为它确实是非常低频修改的内容,但是现在都鼓励在平台上配置了。虽然,我不知道具体是不是有什么故事,但事情就是这么发展了
-
配置的修改频率,以及部署的方式能不能支持到这样的修改频率。比如说,修改环境变量,在内场 tce 平台是一件比较简单的事情。但到我目前使用的自建环境,就需要打包 helm chart,发布版本才行
-
用户相关的配置一般修改频率会高于,代码运行所需要的配置,一般会放在支持高频修改的地方。(我之前没有想过这个点,刚想到的。还需要更多验证)
日志
日志的复杂性,在很多书上是被低估的。日志打不打,什么时候打,打什么,都很有讲究。打多了,不说性能问题,找起来也麻烦。打少了,不够排查问题。有几个可以参考的思路
- 配置文件,读完最好都打出来,有敏感信息的可以脱敏。真的遇到很多次,以为读的是 A 配置,实际跑的是 B 配置的情况
- 出错的时候。那这时候显然不能不打了,不然就不知道为啥出错了
- 逻辑不符合预期的时候。它不是一个错误,只是单纯,你觉得,它应该走到这个分支上来。这个时候,最好也打个点
- 正常运行的时候。这个是最难的,非常依赖业务场景,特别是在高 qps 的场景,不打日志,查不出问题,打日志影响性能。所以有一些系统会有日志染色/加白机制,再加上请求重放功能,基本能解决一些 corner case 导致的异常问题的排查
形象化一点,一个请求经过服务是一条弯曲的路。日志就是里面的标识

模块化
模块化就像文章的段落。我们都知道,文章的段落,段内要统一的去表达这段的中心思想,然后,段间就是少量转折。这就和代码说的高内聚、低耦合是契合的。
模块化有几个好处
-
方便读。脑袋能一次处理的东西是有限的,所有东西搅合在一起,就根本没办法理解。模块化后,我只要关心当前一小块的内容,不需要去关心所有其他代码
-
方便修改。把相关的功能限定在一个比较小的范围内,功能的修改就不会到处改动。真的不要小看这个点,所有改过的代码都是要有风险的,少改代码就是减少没必要的风险
什么是模块化 ?它其实类似于收拾家里的东西的时候,做个分类。比如碗都放碗柜里、衣服都放衣柜里、玩具都放玩具篓子里。如果发现没有合适的容器,就买一个(相当于新建一个文件夹)。写代码就是打点的代码放一起、访问外部依赖的代码放一起 ...。这里真正难的是大项目怎么捋顺。
还有一个点,模块之间不要循环依赖。开始设计模块的时候,就需要分层,模块一般是树状结构,不是线性的。模块设计好之后,应该写进 README 里面,标注好每个模块的层次,不能胡乱引用。比如,常见的 utils 它就很容易去引用业务代码,业务代码再引用 utils,这是非常不对的。utils 它应该是与业务代码无关的代码
但是,我最近用 AI 写代码,发现它会容易胡乱去分文件夹。大部分程序员写代码是层次过少。而 AI 一旦发现循环引用就开始分文件夹,这也是不对的。要先考虑是否是逻辑上有问题,能不能在已有的结构上解决。
错误码
最容易忽视的、最容易凌乱的地方。分几种情况
-
没有错误码,只有错误内容。如果是人读还好,可是如果是写代码判断,就非常不友好。万一某一天,觉得这个错误内容表达的意思不太准确,那一修改就是灾难。不是很常见,但见过
-
错误码定义不清晰。拿到一个错误码以后,感觉什么信息都没有。完全不能辅助排查问题
- 能枚举:给出错误码的列表,清楚列出每种错误的含义。适合错误类型比较有限的场景
- 能区分:最典型的就是 http status code,用数字开头区分不同的错误类型。如果系统有多个模块,最好是模块间有区分,但是同一个错误返回提示都一样。这样看到错误码能快速定位到出问题的模块,又不至于说同一个错误提示五花八门
-
返回给上游的错误类型。比如在 kitex 服务里,业务逻辑错误应该在 base 中返回,而不应该返回一个 err,err 应该是网络错误。
协议
最好是有明确的协议文件。thrift protobuf json 感觉都挺好的,有就行。不想每次去代码里看请求参数。
如果有描述、示例,那就更棒了
插件
业务代码一般可能会比较少用到插件。插件一般适合在有固定节点的地方定制逻辑。什么是固定节点,比如 ES 里面切词的时候,切之前、怎么切、切之后,这样的固定节点里。其实,洋葱模型(middleware)也是有固定节点的,请求前、请求后,所以 gateway 也经常会做成插件式的。
插件的加载/注册方式(动态、静态)、固定节点的设计、稳定性(最好就是插件加载/运行崩溃,不要影响到主逻辑),都是设计的重点。这里内容比较多,不单独展开了
测试
以前测试可能更多是 qa 的任务。但是,随着 AI coding 的不断发展,写测试的重要性会增加。这就和社区开源代码是一个道理,你没办法说每个 PR 都去看一遍,发现有破坏当前逻辑的就打回,那样成本太高了。怎么降低这个成本,就是让 AI 改完以后,先跑一遍单测,看是否有破坏非预期逻辑。可以认为是 AI coding 时代的保命神器,不然你可能都不能发现 AI 给你代码改坏了。
之前有 AI 生成代码没完全按照协议,GET 请求写成 POST 请求的。查了很久。
监控&告警
除了服务本身是否 panic、是否拉起失败(kitex 还提供错误日志变多告警)外。服务的监控主要就是进口、出口的请求量、耗时。业务逻辑本身需要的监控
告警一般是基于监控的。还可以有直接发飞书消息的。告警是监控服务是否发生需要人为干预异常,毕竟我们也不可能每天去看一遍日志。比如说,请求下游突然失败多了、突然走到一个非预期的分支上 ...。
告警和错误码是有关系的。一般告警会过滤掉上游请求错误的错误码,只告警服务本身的异常。如果错误码设计得不好,就有可能很难/挑不出来,需要告警的错误码
写在最后
我们在真正写代码的时候,很难每次都仔细去考虑每一个点,那样确实是太耗时了。有时候会因为一个问题纠结很久,甚至最后都没有答案。可以把它记在心里,下次去看别人代码的时候,就能看到,哦 .... 原来我上次那个解决不好的问题,它能这么解决。
这真的是一条很长的路 ....
关注公众号"字节跳动数据库",获取更多技术干货!