接上篇: twitter snowflake java版.
snowflake算法是依赖于系统时钟的, 而且要求系统时间只能增不能减.
像某些官员一样, 把简单的事情往复杂了说.
好嘛, 只是一本正经地开个玩笑.
在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, 也是让人膈应的事情.