[单排日记][CCD打工纪实]
简介
本文总结了几个月工作中的主要内容:
完成了一个 CRUD 功能模块;开发了一个竞价排名系统,重点在于改进旧项目架构;参与了部分项目重构工作。
旧项目
竞价需求
需要排名的是 1000+ 个对象(支持实时添加),在未竞价时有一套多维度排序体系,维度有优先级,代称为 A、B。对象 1.A 大于对象 2.A 时,对象 1 排在前面;如果 A 相等,再比较 B,以此类推。竞价本身也作为一个维度,优先级最高,但只选出前 N 名排在前面,超过 N 名的退款,并按未竞价的情况排名。
竞价需要设置每次点击扣费额以及总次数。每次扣费额作为排名依据,总次数点击完后不再扣费,同时排名重新按原体系排序。竞价成功后,到期未用完的次数直接参与下一轮竞价。
产品原本要求竞价后即时排序,点击数用完后即时下撤。经过技术团队与产品沟通后,改为每 10 分钟统一更新一次榜单。
难点
排序问题本身不大,这个数量级不需要自行实现排序算法。
项目背景:基于 Node 6 的自研 Web 框架;启动时将部分不常变化的数据及基于这些数据计算的结果存放在全局变量,称为”静态数据”;存在大量自定义全局函数;MySQL 全局封装,不支持事务;Redis 全局封装,仅支持 get 和 set 方法;无守护进程;无负载均衡或多副本;部分数据每月更新,主项目需要冷重启,停服两小时。
Note (2019): 当时项目使用的 Node 6 已于 2019 年 4 月停止维护。现代项目应使用 LTS 版本(如 Node 18/20)并采用成熟的框架(如 NestJS、Fastify 等)。
一个问题是属性 A、B 实际上不固定,且是分库的统计字段。修改需要结合另一个项目手动运行脚本修改数据库再统计,主项目再冷重启。
另一个问题是 10 分钟排一次,中间可以加价,并且需要保存每次的竞价结果。客户端需要查看本人的时序结果,后台既要看到单人的时序、又要看到每个时间节点全部的时序,保留至少 30 天。维度实际上有很多,数据量是一个需要考虑的问题。
解决方案
应用无状态化
考虑到冷启动频繁,不应该有所谓”静态数据”,首先将”静态数据”迁移到 Redis。这些数据一般只在另一个手动脚本运行时才发生改变,因此统一交给该脚本维护。这样主项目即使冷重启,也只读取 Redis 中维护好的数据,而非重新读数据库重算。这部分工作将 Node 主进程占用内存从 900MB 降低到 200MB 左右,也便于后续实现多副本。
之所以保留这套数据,是因为这些数据过于基础,项目中到处使用。这样改才具有可行性,后期会直接重写。如果没有这种历史包袱,预期不会变的数据(如平台名、项目名)应直接写在 config.js 中;任何预期会改变的数据,不管改动是否频繁,都应该存储在数据库中。
守护进程 + 负载均衡
与同事配合完成,主进程用 PM2 管理;负载均衡直接使用阿里云服务,配置端口、重新配置证书即可。后期有专职运维同事加入。
OSS 存储记录
采用 OSS 存储主要考虑成本。只保留 30 天,按日覆盖文件夹,时序数据和全局数据直接生成 JSON。如果前端配合,可以直接在前端进行 OSS 权限验证,数据不经过服务器。
重写和一些思考
重写的主要原因是旧框架存在诸多问题。
这部分由 Node 组配合完成,我负责将之前开发的 CRUD 模块和相关模块用 Egg 重写。
使用完整的 MySQL 事务支持是一个重要改进。旧项目支付体系之所以没出现问题,主要归功于客户量少,未出现过并发场景。但作为技术人员,需要考虑系统在高并发场景下的健壮性。
重写的要点在于按产品需求编写完整的测试用例,尽管这会延长开发周期。在开发质量和效率之间,团队往往选择效率,即使反复强调,也较少考虑”后续维护欠债”的后果。
一些反思
产品的价值不在于特性列表的长度,而在于是否真正解决用户问题,创造实际价值。这需要用事实证明,而非依赖营销话术。