一次简单的分表实践
背景:笔者负责公司内部单点登录系统(inpass)已两年有余,登录服务接入逐渐增多,登录日志逐渐增多。笔者对登录日志也进行了几轮改造和优化,其中涉及到了一次简单的分表操作,特此记录下来。
改造之前的状况(2018.7月调研)
inpass日志表中id采用工具类(SnowflakeIdWorker)自动生成id,但是存在高并发下id重复问题,数据库插入存在主键冲突,导致业务方调用出现异常。同时原有数据库表login_log中数据量已达400w+,统计最近4天的登录日志,平均每天16000条,且有逐渐增多的趋势,因此有必要进行优化。
当时调研了三种方案,如下:
方案1:
将id生成器工具类替换为使用共享服务部提供的ID生成器服务,其他不变。
优点:可快速优化高并发下id重复问题,代码量比较少
缺点:需要增加依赖服务,无法对现有表数据进行拆分优化。
方案2:
重新构建日志表(login_log_new),表字段不变,id改为数据库自增方式。
优点:
- 不依赖外部id唯一工具,
- 可拆分登录日志,减少原有表数据逐渐增大的问题。
缺点:
需要新建表。
方案3:
删除当前日志表(login_log)中3个月之前的数据+方案1.
优点:不需要新建表,
缺点:线上删除大数据量可能会锁表,同时会丢失大量日志数据。
综合考虑,方案2为优。
那上面的方案是否是一劳永逸的呢?
答案是否定的。
我们设想一下,现在的日均日志量已经不是16000+,而是18000+了,
假如以一年为单位,16000365 = 584W条。
18000365 = 657W条。
数据库是MySQL的,场景是插入多,查询少,比例与插入量差不多,但是如果要查的话,那看下面的sql:
select count(*) from login_log where login_time >='2018-07-24 00:00:00' and login_time <='2018-07-24 23:59:59';
这个sql至少执行了3.8秒。应该是可以优化的。
要查该表的背景
- 某员工违规操作,要查日志,什么时间登录的,登录到哪个IP地址。
- 某员工最近一个月通过inpass登录系统的日志。
这种场景非常少,但是在单表这么多数据量的情况下一条sql执行下来估计也需要不少时间。
可以借用分表的设计想法,(分库就不用了哈,没那么多复杂的需求),以年为单位,预先设计3张表login_log_2018(已初始化到线上),login_log_2019(已初始化到线上),login_log_2020(未初始化到线上).
假设每张表存储不超过1KW条数据,以年为单位记,即可满足未来3年内的日志增长量。同时满足所有日志可查。
按上述方案执行之后,到现在已经往login_log_2018表写入了480w登录日志数据了,其实已经到了需要写新表的时候了,此时也接到了需要对inpass进行内审安全合规性的改造,用户需要对登录日志进行查询。新的需求需要对登录过程中的所有操作进行记录,包括修改密码等环节。而原来的登录日志记录仅仅记录登录成功的场景,因此如果再写入数据库就不合适了。
此时需要进行新的改造,由于inpass只是提供单点登录和修改密码的功能,没有查询界面,如果给用户新开列表查询界面也不合适。
经过和领导沟通,有如下操作和方案
- 将登录日志以埋点的方式接入公司的日志平台进行上报。
- inpass服务端使用线程池异步写日志(不阻塞登录主流程)
- 由于接入日志平台之后日志平台可以提供查询界面,所以inpass的改造仅仅在用日志埋点的接入,但是为了保障整个接入过程顺利还进行了双写的操作。
- 就是一方面将登录日志上报到日志平台,一方面写入数据库,此时需要新建表,因为新增了几个记录项,不适合在原有表中扩展,新表为login_log_2020.
- 第4步的操作也为了保障在接入过程中数据可以冗余保留,如果日志平台确实能满足用户查询需求,我们可以很快下掉写数据库的逻辑。