网站开发 app,全网推广代运营,网站运营团队建设,陕西省城乡住房和建设厅网站在复杂分布式系统中#xff0c;往往需要对大量的数据和消息进行唯一标识。如在美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的系统中#xff0c;数据日渐增长#xff0c;对数据分库分表后需要有一个唯一ID来标识一条数据或消息#xff0c;数据库的自增ID显然不能满足… 在复杂分布式系统中往往需要对大量的数据和消息进行唯一标识。如在美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的系统中数据日渐增长对数据分库分表后需要有一个唯一ID来标识一条数据或消息数据库的自增ID显然不能满足需求特别一点的如订单、骑手、优惠券也都需要有唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。概括下来那业务系统对ID号的要求有哪些呢 全局唯一性不能出现重复的ID号既然是唯一标识这是最基本的要求。趋势递增在MySQL InnoDB引擎中使用的是聚集索引由于多数RDBMS使用B-tree的数据结构来存储索引数据在主键的选择上面我们应该尽量使用有序的主键保证写入性能。单调递增保证下一个ID一定大于上一个ID例如事务版本号、IM增量消息、排序等特殊需求。信息安全如果ID是连续的恶意用户的扒取工作就非常容易做了直接按照顺序下载指定URL即可如果是订单号就更危险了竞对可以直接知道我们一天的单量。所以在一些应用场景下会需要ID无规则、不规则。上述123对应三类不同的场景3和4需求还是互斥的无法使用同一个方案满足。 同时除了对ID号码自身的要求业务还对ID号生成系统的可用性要求极高想象一下如果ID生成系统瘫痪整个美团点评支付、优惠券发券、骑手派单等关键动作都无法执行这就会带来一场灾难。 由此总结下一个ID生成系统应该做到如下几点 平均延迟和TP999延迟都要尽可能低可用性5个9高QPS。UUID UUID(Universally Unique Identifier)的标准型式包含32个16进制数字以连字号分为五段形式为8-4-4-4-12的36个字符示例550e8400-e29b-41d4-a716-446655440000到目前为止业界一共有5种方式生成UUID详情见IETF发布的UUID规范 A Universally Unique IDentifier (UUID) URN Namespace。 优点 性能非常高本地生成没有网络消耗。缺点 不易于存储UUID太长16字节128位通常以36长度的字符串表示很多场景不适用。信息不安全基于MAC地址生成UUID的算法可能会造成MAC地址泄露这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。ID作为主键时在特定的环境会存在一些问题比如做DB主键的场景下UUID就非常不适用① MySQL官方有明确的建议主键要尽量越短越好[4]36个字符长度的UUID不符合要求。 All indexes other than the clustered index are known as secondary indexes. In InnoDB, each record in a secondary index contains the primary key columns for the row, as well as the columns specified for the secondary index. InnoDB uses this primary key value to search for the row in the clustered index.*** If the primary key is long, the secondary indexes use more space, so it is advantageous to have a short primary key***. ② 对MySQL索引不利如果作为数据库主键在InnoDB引擎下UUID的无序性可能会引起数据位置频繁变动严重影响性能。 类snowflake方案 这种方案大致来说是一种以划分命名空间UUID也算由于比较常见所以单独分析来生成ID的一种算法这种方案把64-bit分别划分成多段分开来标示机器、时间等比如在snowflake中的64-bit分别表示如下图图片来自网络所示 41-bit的时间可以表示1L41/(1000L*3600*24*365)69年的时间10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求还可以将10-bit分5-bit给IDC分5-bit给工作机器。这样就可以表示32个IDC每个IDC下可以有32台机器可以根据自身需求定义。12个自增序列号可以表示2^12个ID理论上snowflake方案的QPS约为409.6w/s这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。 这种方式的优缺点是 优点 毫秒数在高位自增序列在低位整个ID都是趋势递增的。不依赖数据库等第三方系统以服务的方式部署稳定性更高生成ID的性能也是非常高的。可以根据自身业务特性分配bit位非常灵活。缺点 强依赖机器时钟如果机器上时钟回拨会导致发号重复或者服务会处于不可用状态。应用举例Mongdb objectID MongoDB官方文档 ObjectID可以算作是和snowflake类似方法通过“时间机器码pidinc”共12个字节通过4323的方式最终标识成一个24长度的十六进制字符。 数据库生成 以MySQL举例利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增每次业务使用下列SQL读写MySQL得到ID号。 begin;
REPLACE INTO Tickets64 (stub) VALUES (a);
SELECT LAST_INSERT_ID();
commit;这种方案的优缺点如下 优点 非常简单利用现有数据库系统的功能实现成本小有DBA专业维护。ID号单调自增可以实现一些对ID有特殊要求的业务。缺点 强依赖DB当DB异常时整个系统不可用属于致命问题。配置主从复制可以尽可能的增加可用性但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。ID发号性能瓶颈限制在单台MySQL的读写性能。对于MySQL性能问题可用如下方案解决在分布式系统中我们可以多部署几台机器每台机器设置不同的初始值且步长和机器数相等。比如有两台机器。设置步长step为2TicketServer1的初始值为11357911…、TicketServer2的初始值为2246810…。这是Flickr团队在2010年撰文介绍的一种主键生成策略Ticket Servers: Distributed Unique Primary Keys on the Cheap 。如下所示为了实现上述方案分别设置两台机器对应的参数TicketServer1从1开始发号TicketServer2从2开始发号两台机器每次发号之后都递增2。 TicketServer1:
auto-increment-increment 2
auto-increment-offset 1TicketServer2:
auto-increment-increment 2
auto-increment-offset 2假设我们要部署N台机器步长需设置为N每台的初始值依次为0,1,2…N-1那么整个架构就变成了如下图所示 这种架构貌似能够满足性能的需求但有以下几个缺点 系统水平扩展比较困难比如定义好了步长和机器台数之后如果要添加机器该怎么做假设现在只有一台机器发号是1,2,3,4,5步长是1这个时候需要扩容机器一台。可以这样做把第二台机器的初始值设置得比第一台超过很多比如14假设在扩容时间之内第一台不可能发到14同时设置步长为2那么这台机器下发的号码都是14以后的偶数。然后摘掉第一台把ID值保留为奇数比如7然后修改第一台的步长为2。让它符合我们定义的号段标准对于这个例子来说就是让第一台以后只能产生奇数。扩容方案看起来复杂吗貌似还好现在想象一下如果我们线上有100台机器这个时候要扩容该怎么做简直是噩梦。所以系统水平扩展方案复杂难以实现。ID没有了单调递增的特性只能趋势递增这个缺点对于一般业务需求不是很重要可以容忍。数据库压力还是很大每次获取ID都得读写一次数据库只能靠堆机器来提高性能。Leaf这个名字是来自德国哲学家、数学家莱布尼茨的一句话 There are no two identical leaves in the world “世界上没有两片相同的树叶” 综合对比上述几种方案每种方案都不完全符合我们的要求。所以Leaf分别在上述第二种和第三种方案上做了相应的优化实现了Leaf-segment和Leaf-snowflake方案。 Leaf-segment数据库方案 第一种Leaf-segment方案在使用数据库的方案上做了如下改变 - 原方案每次获取ID都得读写一次数据库造成数据库压力大。改为利用proxy server批量获取每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段可以大大的减轻数据库的压力。 - 各个业务不同的发号需求用biz_tag字段来区分每个biz-tag的ID获取相互隔离互不影响。如果以后有性能需求需要对数据库扩容不需要上述描述的复杂的扩容操作只需要对biz_tag分库分表就行。 数据库表设计如下 --------------------------------------------------------------------------------------
| Field | Type | Null | Key | Default | Extra |
--------------------------------------------------------------------------------------
| biz_tag | varchar(128) | NO | PRI | | |
| max_id | bigint(20) | NO | | 1 | |
| step | int(11) | NO | | NULL | |
| desc | varchar(256) | YES | | NULL | |
| update_time | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
--------------------------------------------------------------------------------------重要字段说明biz_tag用来区分业务max_id表示该biz_tag目前所被分配的ID号段的最大值step表示每次分配的号段长度。原来获取ID每次都需要写数据库现在只需要把step设置得足够大比如1000。那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step大致架构如下图所示 test_tag在第一台Leaf机器上是1~1000的号段当这个号段用完时会去加载另一个长度为step1000的号段假设另外两台号段都没有更新这个时候第一台机器新加载的号段就应该是3001~4000。同时数据库对应的biz_tag这条数据的max_id会从3000被更新成4000更新号段的SQL语句如下 Begin
UPDATE table SET max_idmax_idstep WHERE biz_tagxxx
SELECT tag, max_id, step FROM table WHERE biz_tagxxx
Commit这种模式有以下优缺点 优点 Leaf服务可以很方便的线性扩展性能完全能够支撑大多数业务场景。ID号码是趋势递增的8byte的64位数字满足上述数据库存储的主键要求。容灾性高Leaf服务内部有号段缓存即使DB宕机短时间内Leaf仍能正常对外提供服务。可以自定义max_id的大小非常方便业务从原有的ID方式上迁移过来。缺点 ID号码不够随机能够泄露发号数量的信息不太安全。TP999数据波动大当号段使用完之后还是会hang在更新数据库的I/O上tg999数据会出现偶尔的尖刺。DB宕机会造成整个系统不可用。双buffer优化 对于第二个缺点Leaf-segment做了一些优化简单的说就是 Leaf 取号段的时机是在号段消耗完的时候进行的也就意味着号段临界点的ID下发时间取决于下一次从DB取回号段的时间并且在这期间进来的请求也会因为DB号段没有取回来导致线程阻塞。如果请求DB的网络和DB的性能稳定这种情况对系统的影响是不大的但是假如取DB的时候网络发生抖动或者DB发生慢查询就会导致整个系统的响应时间变慢。 为此我们希望DB取号段的过程能够做到无阻塞不需要在DB取号段的时候阻塞请求线程即当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。这样做就可以很大程度上的降低系统的TP999指标。详细实现如下图所示 采用双buffer的方式Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时如果下一个号段未更新则另启一个更新线程去更新下一个号段。当前号段全部下发完后如果下个号段准备好了则切换到下个号段为当前segment接着下发循环往复。 每个biz-tag都有消费速度监控通常推荐segment长度设置为服务高峰期发号QPS的600倍10分钟这样即使DB宕机Leaf仍能持续发号10-20分钟不受影响。每次请求来临时都会判断下个号段的状态从而更新此号段所以偶尔的网络抖动不会影响下个号段的更新。Leaf高可用容灾 对于第三点“DB可用性”问题我们目前采用一主两从的方式同时分机房部署Master和Slave之间采用半同步方式[5]同步数据。同时使用公司Atlas数据库中间件(已开源改名为DBProxy)做主从切换。当然这种方案在一些情况会退化成异步模式甚至在非常极端情况下仍然会造成数据不一致的情况但是出现的概率非常小。如果你的系统要保证100%的数据强一致可以选择使用“类Paxos算法”实现的强一致MySQL方案如MySQL 5.7前段时间刚刚GA的MySQL Group Replication。但是运维成本和精力都会相应的增加根据实际情况选型即可。 同时Leaf服务分IDC部署内部的服务化框架是“MTthrift RPC”。服务调用的时候根据负载均衡算法会优先调用同机房的Leaf服务。在该IDC内Leaf服务不可用的时候才会选择其他机房的Leaf服务。同时服务治理平台OCTO还提供了针对服务的过载保护、一键截流、动态流量分配等对服务的保护措施。 Leaf-segment方案可以生成趋势递增的ID同时ID号是可计算的不适用于订单ID生成场景比如竞对在两天中午12点分别下单通过订单id号相减就能大致计算出公司一天的订单量这个是不能忍受的。面对这一问题我们提供了 Leaf-snowflake方案。 Leaf-snowflake方案完全沿用snowflake方案的bit位设计即是“1411012”的方式组装ID号。对于workerID的分配当服务集群数量较小的情况下完全可以手动配置。Leaf服务规模较大动手配置成本太高。所以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。Leaf-snowflake是按照下面几个步骤启动的 启动Leaf-snowflake服务连接Zookeeper在leaf_forever父节点下检查自己是否已经注册过是否有该顺序子节点。如果有注册过直接取回自己的workerIDzk顺序节点生成的int类型ID号启动服务。如果没有注册过就在该父节点下面创建一个持久顺序节点创建成功后取回顺序号当做自己的workerID号启动服务。 弱依赖ZooKeeper 除了每次会去ZK拿数据以外也会在本机文件系统上缓存一个workerID文件。当ZooKeeper出现问题恰好机器出现问题需要重启时能保证服务能够正常启动。这样做到了对三方组件的弱依赖。一定程度上提高了SLA 解决时钟问题 因为这种方案依赖时间如果机器的时钟发生了回拨那么就会有可能生成重复的ID号需要解决时钟回退的问题。 参见上图整个启动流程图服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点 若写过则用自身系统时间与leaf_forever/${self}节点记录时间做比较若小于leaf_forever/${self}时间则认为机器时间发生了大步长回拨服务启动失败并报警。若未写过证明是新服务节点直接创建持久节点leaf_forever/${self}并写入自身系统时间接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IPPort然后通过RPC请求得到所有节点的系统时间计算sum(time)/nodeSize。若abs( 系统时间-sum(time)/nodeSize ) 阈值认为当前系统时间准确正常启动服务同时写临时节点leaf_temporary/${self} 维持租约。否则认为本机系统时间发生大步长偏移启动失败并报警。每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}。由于强依赖时钟对时间的要求比较敏感在机器工作时NTP同步也会造成秒级别的回退建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE等时钟追上即可。或者做一层重试然后上报报警系统更或者是发现有时钟回拨之后自动摘除本身节点并报警如下 //发生了回拨此刻时间小于上次发号时间if (timestamp lastTimestamp) {long offset lastTimestamp - timestamp;if (offset 5) {try {//时间偏差大小小于5ms则等待两倍时间wait(offset 1);//waittimestamp timeGen();if (timestamp lastTimestamp) {//还是小于抛异常并上报throwClockBackwardsEx(timestamp);} } catch (InterruptedException e) { throw e;}} else {//throwthrowClockBackwardsEx(timestamp);}}//分配ID 从上线情况来看在2017年闰秒出现那一次出现过部分机器回拨由于Leaf-snowflake的策略保证成功避免了对业务造成的影响。 Leaf在美团点评公司内部服务包含金融、支付交易、餐饮、外卖、酒店旅游、猫眼电影等众多业务线。目前Leaf的性能在4C8G的机器上QPS能压测到近5w/sTP999 1ms已经能够满足大部分的业务的需求。每天提供亿数量级的调用量作为公司内部公共的基础技术设施必须保证高SLA和高性能的服务我们目前还仅仅达到了及格线还有很多提高的空间。 照东美团点评基础架构团队成员主要参与美团大型分布式链路跟踪系统Mtrace和美团点评分布式ID生成系统Leaf的开发工作。曾就职于阿里巴巴2016年7月加入美团。 最后做一个招聘广告如果你对大规模分布式环境下的服务治理、分布式会话链追踪等系统感兴趣诚挚欢迎投递简历至zhangjinlu#meituan.com。 施瓦茨. 高性能MySQL[M]. 电子工业出版社, 2010:162-171.维基百科UUID.snowflake.MySQL: Clustered and Secondary Indexes.半同步复制 Semisynchronous Replication.