入侵降噪工程重构心得分享

发布时间 2023-10-21 20:45:39作者: 琴水玉

做完一件事,要及时总结经验教训。


系统重构,属于技术性需求。通常是现有实现难以满足某些非功能属性而产生的。这些非功能属性,通常与性能、可扩展性等有关。

系统重构,就是只改变内部实现,不改变外部行为。也就是“换骨不换皮”。你可能全身都变成机器人了,但表面看上去与原来的你无异。

之前算法组有位刚接触 Java 的新同学,构建了一个降噪工程。虽然工程规范和性能方面还不太好,但能够搭起这个工程,也是很不错了。只是性能和水平扩容能力没法过测试同学这一关,故而需要做一次重构。

重构心得

重构如何着手

系统重构如何着手呢?

  • 分析现有系统的不足;
  • 提出主要改进点;
  • 新的主要技术决策;
  • 改造系统代码,自测通过。

比如这次入侵工程改造,主要改进点如下:

此外,原降噪工程的水平扩容能力较差,也是一个重要改进点。

沟通交流

重构工程,在技术上没有太大难点的情况下,最重要的是沟通与交流。

  • 比如主要技术决策,在原有入侵工程里添加一个降噪模块,还是单独起一个新的降噪工程,是否用其它方式替代 Flink, 都需要与 leader 和总技术负责人讨论和确认,得到 leader 和总技术负责人的首肯。这一点需要在技术评审会议上明确好。
  • 好的技术决策,需要通过讨论来产生。比如降噪工程的原来采用租户ID为分区键,如果某个租户的告警特别多,或者独立部署只有一个租户,那么基本起不到水平扩容的能力。与朱哥讨论,能否用“租户ID+规则ID”比单用租户应该好很多。但朱哥提到某个规则的告警量非常大,也可能导致扩容效果大打折扣。最后经过讨论,发现降噪是以”模型hash“来查询和更新的,故分区键改为”模型hash“。
  • 改造原工程,不可避免有很多逻辑不是太清楚,就需要时不时与原作者沟通和确认一些技术细节。

具体改造工作

  • 尽量复用已有工作。经过讨论,要起一个新工程,就复用了已有工程的脚手架和很多配置、代码,让重构工作从 50% 开始,而不是从零开始,提升了重构开发效率。
  • 对于 Json 转换,虽然有第三方的转换库,但还是决定手动改造,主要是为了熟悉相应的业务逻辑。一个个转换,然后通过单测来验证。
  • 因为有多个告警类型的降噪,但模式流程很相似,就适合应用策略模式来改写,让流程清晰和容易复用。
  • 使用 IDE 功能简化工作量。比如有些方法名不符合 Java 编程规范,就可以使用 IDEA 的 refactor 功能来重构,一键全部修改,包括引用的地方;比如全局查找和替换能力。

不放过一个错误

中间有个小插曲。就是我一重新部署整包,就有一个地方报 NPE,但是单独部署,打了调试日志,却发现 NPE 又没有了。

显然,behaviorModelProfile.setReductionProcessCommand 和 p.getReductionProcessCommandSerial() 都不会抛 NPE。因为如果要抛 NPE,在之前的代码就应该抛出了。那么 tsgExec 是否可能抛 NPE 呢。打了 debug 日志,发现非空。这下可十分困惑了。想来想去想不到原因,就暂时先搁下了。
结果,提测之后,这个问题在测试环境又复现了。
经过仔细排查,发现是组件初始化顺序的问题。当时简便起见,tsgExec 这个变量就直接用一行静态方法 initTsgExec(config) 初始化了。但是在这个静态方法执行之前,某个依赖tsgExec 实例的类已经实例化,且因为这时候静态方法还没有执行,获取 tsgExec 为空,导致运行时就抛 NPE 了。这个问题是偶现的。
为什么静态方法初始化不靠谱?因为它不受 Spring bean 管理。如果是 Spring bean 管理, Spring 会自动把所有 bean 的依赖关系理顺,然后按照依赖关系去实例化,这样就能保证所有的组件正确实例化,且注入合适的依赖实例。但如果是静态方法,它与 Spring bean 实例化是独立的,这可能导致它初始化时去获取实例,而实例实际上还没实例化成对象,就会获取到空对象。

耐心很重要

起初拿到这个工程重构的事情,压力还比较大的,特别是那一堆难以琢磨的很多各种 JSON 字符串到 JSONObject 到对象之间的解析和转换代码,想想都有点头疼。你都不知道 JSONObject 里面究竟有什么。

给点例子感受下:

耐心很重要。最后通过经验和猜测,再加上单测的辅助,把这些 JSONObject 和对象之间的转换都摆平了,也添加了相应的单测。

幸好这个工程属于初创期,代码量不多,因此单个人改造也能搞定了。

重构之缘

说起来,我与重构还是有一些缘分。

  • 在阿里云的时候,就把 flex 用 Java + Sping + Extjs 重写了(具体是怎么完成的忘记了)。原因是 Extjs 的颜值很高,而 flex 编译很慢。emmm,就前端技术来说,颜值也是很重要的。后来嫌 Extjs 太重,又用 jQuery 和 bootstrap 重写了一把。虽然 Extjs 现在看来似乎有点过时,但它的配置化思想给我留下了比较深的印象。它遵从 MVC 思想,组件的外观和行为都是可配置的。这也给我后面做订单导出配置化提供了一些预先的“熏陶”。要知道,很多代码都是纯过程式代码,而我能早早接触配置化代码,对形成好的设计思维是有帮助的。
  • 在有赞的时候,刚开始接手订单导出。与大数据团队合作,将基于 PHP 的订单导出重构成基于 ES + HBase 的订单导出,1-2w 订单导出再也不阻塞了,1min 可以导出 1w 订单,后面优化成可以导出几百万订单,与这次技术重构干系很大。大数据团队也是出力很多,项目初期的很多代码基本是他们操刀的。详见: “有赞订单导出的配置化实践”。
  • 在有赞的时候,还经历过一次新交易重构项目。把整个交易重写了,基于 DDD 的思想,用组件编排的思想进行交易流程编排。这个也给我很深的印象。由此,我到青藤云安全之后,用组件编排的思想把绝大多数入侵检测流程重构了一遍。现在入侵检测流程的实现,再也不需要来一个告警类型就前后端都重新搞一遍(包括新的表设计、流程设计),只要做一点配置就可以了。
  • 我个人也是通过持续的代码重构,去逐渐提升编程技艺的。见 “代码修行分类”。

当然,所有的项目都离不开团队的共同努力。我很感谢和我共事的小伙伴们,他们实力强,性格比较温和,对代码质量也很注重,真使我受益匪浅。

小结

完成了这样一个小小的技术挑战,与做完“订单同步工程标准化改造事记”有相似的感觉。就是接到手时和完成之前持续有一些压力,但做完之后就感觉风轻云淡,又迈过了一个小山头。当然,订单同步改造那可是顶着几十万商家的几千万订单流量“作案”,发布那天压力巨大;但这个只是初创系统,使用的客户也比较少,压力小很多,只是做起来有点烦恼。

做完一件事,要及时总结经验教训,如果能建立方法论更好。

足够多足够大的挑战,才能促进人的成长。