twitter snowflake生成流水号的系统时间后调问题

接上篇: twitter snowflake java版.

 

snowflake算法是依赖于系统时钟的, 而且要求系统时间只能增不能减.  

 

像某些官员一样, 把简单的事情往复杂了说. 

假设系统时间为t', 现实世界时间为t, ntpd为时间服务, 则函数t'=ntpd(t)在[大爆炸, 审判日]区间是增函数且具有单调性. 

 好嘛, 只是一本正经地开个玩笑.

 

在twitter的官方scala实现中, 对于时钟后调的场景是直接抛出异常, 由上层程序来处理. 

    if (timestamp < lastTimestamp) {
      throw new InvalidSystemClock("Clock moved backwards.  Refusing to generate id for %d milliseconds".format(
        lastTimestamp - timestamp))
    }

这样的做法是可以理解的, 目的是为了避免生成重复流水号. 流水号重复的后果, 可大可小, 要看上下游程序怎么写了,可能导致事务失败, 也可能破坏数据一致性, 甚至导致数据丢失.

 

参考ntpd文档http://doc.ntp.org/4.1.0/ntpd.htm, 如果误差在128ms以内, 则ntpd会对系统时钟进行渐变(slew), 直到和ntp server一致为止, 此时系统时钟会略微变快或者变慢, 但依然是单调增加的, 所以对于snowflake来说是安全的. 渐变时, 每一秒最多变慢或者变慢0.5ms; 所以如果误差为128ms, 则最少需要256s才能调整过来.

 

如果超过128ms, 就会进行跳变(step), 此时时间是不连续的, 如果往后调, snowflake就要出错.

 

 

一般在ntp服务刚启动的时候, 误差较大, 此时可能误差几秒到几十个小时; 如果BIOS断电后重启, 则误差可能几十年. 此时ntp服务必然会进行跳变, 则snowflake出错就是大概率事件.

 

说完问题, 来说解决办法. 完美的办法还没有找到, 只能尽可能增大可用性

 

解决办法一:

增大跳变阈值. 使用tinker step命令, 参见文档http://doc.ntp.org/4.1.2/miscopt.htm. 默认阈值是128ms.

可以将阈值增大为10s, 也就是需要5.5个小时才会渐变到一致.

这样做并不能从根本上解决问题, 只是降低了snowflake出错的可能.

 

解决办法二:

在/etc/ntp.conf中添加-x参数, 效果是禁用跳变.

这样, 如果误差达到panic阈值, 默认1000s, ntpd就会自动退出, 并在/var/log/messages中写入日志.

(可以用tinker pannic设置panic的阈值, 参见http://doc.ntp.org/4.1.2/miscopt.htm.)

如果设置了cron来扫描/var/log/messages, 则可以自动给管理员发出预警邮件.

管理员收到邮件后, 先将snowflake程序关闭, 执行ntpupdate, 并通过已经生成的流水号确定误差范围, 比如3000s. 则3000秒之后再启动snowflake程序. 

这个办法是个笨办法, 但在实际应用中不会太痛苦, 因为这种大误差毕竟是小概率事件.

 

解决办法三:

在/etc/ntc.conf中添加-q参数, 效果是执行一次时间同步ntpd就立即退出.

这是一种鸵鸟政策,代价是分布式环境中可能出现各个系统的时间不一致.但是,其负面作用还是可控的. 比如, 在写入数据库的时候, 尽可能不要读取当前应用服务器的时间, 而是使用数据库的时间函数. 

解决办法四:

考虑修改snowflake算法, 不再返回long而是返回字符串.

当发现时钟快了时, 直接返回UUID字符串. 虽然UUID也可能重复, 不过引入了一些随机性, 冲突的可能性很小.

这个方法能极大地提高可用性, 属于软着陆, 在时钟过快的情况下能将冲突控制在近乎可以忽略的程度. 而且极少量的UUID, 也不会影响数据库索引的效率. 不足之处是改动较大, 甚至可能会修改涉及的表结构. 而在一堆流水号中出现一个丑陋的UUID, 也是让人膈应的事情.

 

 

 

猜你喜欢

转载自rickgong.iteye.com/blog/2367580