前几天一个事情吵吵好几天,其实说简单也简单,不过各自站的角度不同,也有对对方实现的不理解。
起因
起因是,端上要实现一个周期性启动的任务,最终决定由服务端来调度启动。因为一些历史原因,原先的启动是通过修改一个云控配置来实现。所以就期望服务端调度启动的时候,也是通过下发一个同样的配置(不是简单的信息,是比较复杂的yaml文件)来实现。由于当时没考虑太多,就同意了这种实现方式,于是就埋了一个坑。
后续就是正常功能开发,上线。
问题
上线后发现这个任务想要运行起来,还需要开启一个端上的配置开关才能运行,所以在每次调度启动的时候还需要同步下发一个开关配置。也就是说服务想要调度这个功能运行需要下发两个信息(开关+调度配置)。
其实在做这个的时候我也知道有这个开关,本来是想在功能上线之后,配置一个对全量设备生效的开关就 OK,但后来发现远没有这么容易。
首先这个开关开启后,端上将自动启动一次任务。那么如果全量开启,意味着所有设备将在同时启动这个任务。但业务方对这个状态是无法接受的。认为这是一个支线任务,只能均匀的调度启动(最多只能有 1/7 的设备同时启动),而不能一下都启动,导致主线任务无法进行(无法回传核心数据)。但其实就算均匀启动,在一个运行周期内做这任务的用的总时间是一样的。
其次端上在使用这个配置的时候,会跟本地已经存在的配置做一个 merge。这意味着每个设备上的这个配置内容都可能不同,所以就不能简单的下一个全局的配置,而是要先读取端上正在使用的配置,将这个配置中的启动开关打开之后再下发到端上。
接着就是好几轮的掰扯了。
端上希望服务下两个配置:开关+调度配置,在每次调度前先检测下开关是否关闭,如果关闭状态,需要先下发一个配置将其打开,而后再下发调度配置。
而服务这一直坚持:只下发调度配置,开关不应由服务控制。
服务为什么坚持呢?原因主要有三个。
- 遵循高内聚低耦合的思想,模块之间的交互就是应该做到最简单。其实最合理的方案也不是这样。服务本不应该下发复杂的 yaml 配置文件,而应该只下发一个任务启动的信号(也就是前面说过埋的坑)
- 这个存在的开关意味着有手动开关的需求,如果服务调度之前就启动下,那跟没有有什么区别?如果调度贸然改了手动下发的配置,有可能会产生业务问题。调度不应该去干预业务内部的控制逻辑。
- 只下发配置,意味着更少的代码量,更少的数据库记录数,如果能用更少的代码,更少的数据量来实现一样的功能,为什么不呢?
解决
但最后还是谁也没说服谁,问题上升了,由老板组了个场子又扯了一遍。
最终的解决方式是:
在灰度验证完之后,将启动全量调度,端上的调度开关后续会默认打开(在端上存在的默认配置会改为打开)。
- 在端上开关默认打开之前,由服务下发:开关+调度配置
- 在端上开关默认打开之后,服务只下发:调度配置
于是我将打开开关的操作,单独拆了一个调度任务出来。这样就可以在不改变原有代码的前提下,也可以满足端上任务启动的目的。在全量之后,直接把这个调度任务关闭就可以了。
感想
整个事情之后,我想了下,为什么要弄的这么复杂?
其实最开始的时候,如果能坚持使用发送信号的方式,就没有后续这些事情了,而当时的妥协导致了后续的争论。所以这个也告诫我,要坚持做对的事情,不能因为种种原因妥协。前期的妥协可能带来巨大的坑,后期争论带来的负面反馈绝对比前期妥协带来的正面反馈要多的多。同时也看到,系统与系统、端与端、模块与模块之间的交互就是要做到信息传递的最小化,千万不要去控制其他模块的内部逻辑。
其实,中间我也几次想要妥协,因为按照那种方式也可以实现。但做事情永远是:由该不该做决定,而不是能不能做。程序对需求的实现能有很多种,但最优的只有那一种。就像对 2 这数字来说,1 << 1 = 2;1 + 1 = 2;99999 - 99997 = 2;这三种实现,哪个对计算机的计算成本最低呢?最近看了几个机器学习的视频,对一个模型的评价会使用 Loss 函数,其实我们在评价需求实现的时候,也是需要有个自己的 Loss 函数。