• Feeds

  • 多IDC数据时序问题及方法论

    上一周在微博架构与平台安全演讲中提到多IDC及架构设计的方法,由于最近工作中经常碰到这种情况,再举一个小案例补充一下。

    Web数据访问比较好的设计模式是使用cursor方式(参考前文用Twitter的cursor方式进行Web数据分页),原理上相当于增量方式访问数据,可以极大提高访问性能。

    在单IDC场景中,如图1,系统的id是递增,假设用户上一次访问最新一条记录是1002,则本次访问最佳的方式是 get?cursor=1002,可以高效取到后面3条新记录。

    多IDC场景,看图2,假设白色背景属于Region 1,灰色背景属于Region 2, 由于两地同步有延迟,这样在Region 1中1001和1003来到时间较晚,排在本地数据1002和1004后面。假设用户上一次也是取到最新一条是1002(注意此时1001没取到,因为从外地未同步过来)。在Region 1调用 get?cursor=1002返回结果会得到什么?从数据库角度来看,访问cursor=1002 只会取到id>1002的记录,而上次未取到的1001即使已经同步过来是永远不会返回了。这样就产生了数据一致性问题,1001丢了。另外一个机房Region 2调用也产生类似问题。不同的cursor产生不同的丢失问题。

    提出这个问题后身边很多技术人员非常感兴趣,经常走在路上被拦住介绍他们突然想到的一种更巧妙的解决方法。部分思路如下
    (这里先不考虑ID递增算法如何实现,多IDC使用K-SORT方式递增也是比较容易的)

    • 例外的方式,把迟到的id都存下来
    • 补方式,把cursor往前多取一点,宁滥毋缺
    • 快照方式,最近取的记录都存下来,这样服务器内部知道这个cursor上次哪些id取了哪些没取

    大部分方法貌似都能工作,但都有问题或不完美,更重要的一点,也就是上周演讲中提到的,架构要把复杂的问题抽象简单,很多技术人员面对这个问题,并没有深层次思考这个场景的问题本质是什么,因此虽然匆匆考虑了很多复杂的解决方案,但是没有完美解决问题。

    有兴趣的朋友可以继续思考,看能否将复杂的问题抽象简单并解决?

    如想及时阅读Tim Yang的文章,可通过页面右上方扫码订阅最新更新。

    « | »

    26 Comments  »

    1. yamingd

      可以用时间戳的方式,每条记录到达时产生一个时间戳(精确到微秒)ts(整数存储)
      1. 每次拿到cursor,先看看这条记录的时间戳v_cursor_ts (可以永久缓存起来,因为这个时间戳是永远不会变的)
      2. 那么只要列出 ts > v_cursor_ts 的记录就可以让用户永远看到TA该看的记录
      ————————————————-
      ok.

    2. peterhan

      所有id记录从一开始都按顺序在各地区插入,被条跳过的id留空记录,之后同步时补到相应位置

    3. doordie

      本质是时间线不一致的问题
      所以把每个IDC的CURSOR都传递过去是否可行?

    4. @timYang 由于多机房的延迟,依靠时间戳肯定是不靠谱的。
      1.如果你不使用缓存的话,当延迟的数据同步后,CURSOR经过排序后也是没有问题的。
      2.为了追求高性能,可能会采用最佳方式的数据结构,一旦写入,就不可更改顺序。这种方式可以给同步的数据打上时间标签。每个机房之间的时间不同步有个最大差值。我们可以通过各个机房同步过来的数据的时间标签和最大差值对比,判断同步延迟的时间。超过同步延迟时间的数据表示是完整同步的内容。通过这种思想的话,可以先临时记录同步的数据【内存磁盘均可】,按照完整同步的时间区域往持久存储移动。查询的话需要做两个数据库的merge即可。
      欢迎探讨。

    5. 本质上是多IDC数据中心数据不一致,并且web客户端采用cursor方式读取数据导致的数据不一致的问题。感觉可以从两个方面试试看:
      1、可以从数据方面着手,看能否限制多IDC中心的数据同步的时候,按照顺序来同步数据保证数据的一致性?
      2、从客户端调用的api方面着手来实现数据一致性,目前大部分再讨论的是这个方面。但是由于数据的不一致性,这里的控制就比较复杂,既要达到保持数据的一致性,又要保证高速的读写效率。由于数据一致性很难保证,也比较难判断具体的记录谁先谁后。但是我认为只要所有的上次没有读出的记录,再下一次查询能够读出即可。因为分页也是连续的,能否采用一种类似队列的方式来读取,保证后到的记录也可以被读出?

    6. Leo

      ID和时间戳上都建索引,get?cursor=1002在各地IDC中实际上完成的操作是:
      select timstamp as t from weibos where id = 1002;
      select * from weibos where timstamp > t limit xxx;

      在data center中id与timestamp的递增顺序是一致的,在分IDC中不一致。

    7. zh

      既然是多IDC导致的问题,能否在各IDC产生ID时就加入本IDC的标志信息,在后续处理时差别处理再统一返回客户,不过这样对IDC变化的时候不是很方便,需要在程序实现时考虑可配置项。另外,感觉实现方式也与数据存储方式有关,是存在mysql、还是一些nosql的软件里。

    8. zh

      通过tim的文章或转介绍,学到不少业界最新动态,感谢分享,微博加油!

    9. gengmao

      如果id是连续递增的,客户端可以保存跳过的id,下次从最小的跳过的id查起。这样服务器不用特别处理时序问题。对吗?

    10. 从严格意义上来讲,例子中 get?cursor=1002 获取的最后一条记录1002,并不是能保证一致性的结果,所以会导致后续问题。

      一个简单优美的解决方案是牺牲区域内的实时性,保证数据的准确性。即IDC数据同步时记录下已同步的最新时间戳(或者记录id),每一次返回结果都是IDC同步后的数据。

      如果需要保留区域内的实时性,那得修改协议接口。每一次返回数据的同时,得告知客户端一个能保证数据一致性的最大记录id(可能比数据中的最大id小);刷新时,从上一次返回的一致id 作为 cursor 参数请求。

    11. 首先不能引入时间参数。

      数据层面:
      按连续ID来的话,每次多取Pagesize-(Max-Cursor)条记录即可。
      最坏是O(Pagesize),但这种情况在新浪围脖里会比较少。
      这种方法得统计并分析。

      架构层面:
      数据状态?

    12. 莫非新浪微博在用关系型数据库?
      为什么不是用No-SQL的key-value数据库呢?如果是key-value形式的数据库我觉得可以这么做:
      先说整体的存储方案,一个用户的数据以一种链表的形式保存;即每条信息都有两个属性分别指向它的前一条数据和后一条数据;
      当ragion1的某些数据需要同步到ragion2时可以根据这些数据在ragion1链表中的位置 检查他们在ragion2中是不是链表的末尾;如果不是,说明数据同步出现了延迟,那么就改变数据属性中的指向,把这些数据添加到链表末尾。这样无论从哪个ragion读取能保证数据不会出现遗漏了。
      关于数据删除:每条数据肯定都有一个唯一的key是可以直接定位的,通过key从各个ragion删除,并在删除前根据数据里的指向找到前后两条数据修改指向,避免链表断裂。
      关于分页:可以添加其他辅助数据结构实现分页等等功能

    13. store88

      看了这么多留言
      我觉得可以用程序增强数据一致性
      既然已经知道当前的cursor是1002
      那么每同步一条数据的时候,可以跟当前cursor比较
      比如如果同步的数据id是1001, 可以替换1002为1001

      一般来说,最终的方案在于你愿意花多大的性能损耗来保证数据的一致性
      极端情况下就只能串行了

    14. tiger

      如果id是连续递增的,客户端可以保存跳过的id,下次从最小的跳过的id查起。这样服务器不用特别处理时序问题。对吗?

      —–跟我想的一样,^_^

    15. luguo...

      tiger says:”如果id是连续递增的,客户端可以保存跳过的id,下次从最小的跳过的id查起。这样服务器不用特别处理时序问题。对吗?”
      如果目前的ID是1004,延迟的是100,你也从100查?

    16. luguo...

      to tiger :且如果延迟比较严重和频繁,某个时刻几乎等于查全部吧,当前10001,突然来个ID=1,感觉还是从用户访问的数据入手吧,按需分配。

    17. luguo...

      我有点激动那~ohye

    18. Tim

      本文在新浪微博上面有更多讨论及留言,有兴趣可以去围观。http://t.sina.com.cn/10503/zF0tex7z7b (需登录)

    19. Willwil

      既然是多IDC导致的问题,能否在各IDC产生ID时就加入本IDC的标志信息,在后续处理时差别处理再统一返回客户,不过这样对IDC变化的时候不是很方便,需要在程序实现时考虑可配置项。另外,感觉实现方式也与数据存储方式有关,是存在mysql、还是一些nosql的软件里。
      —————————————–
      这种方式应该更General,而不是打补丁。为每个IDC的数据生成不同的标记or 独立的ID,每条数据有GUID。IDC的ID是顺序递增的,cursor以此ID为依据。

      不过这也有一个问题,涉及对现有系统的改造,影响可能比较大。

    20. guping

      既然不同的region对应互不重叠的id,返回的cursor其实应该是一组cursor,每个cursor代表一个region,查找下一个返回的数据应该是取所有有可用数据里面id最小的那个。

    21. chris

      要看对ID操作的类型

      * 如果是查询\统计类操作,需要的是实时结果,不需考虑丢失的cursor

      * 如果包含后续处理,则需要考虑“长丢失”/“长乱序”场景,即luguo对tiger举的例子,此时需要“窗”来控制(时间或数量),Master可选push方式(Master单点负担重),或者标记salve-cursorID对,salve下一次请求时以获取

    22. Whisper

      问题的核心在于,距离产生的延迟是客观存在的。

      能做的无非提高传输速度,或者绕开这个问题。
      第一个基本不可能。

      直接考虑第二个。

      于是问题转化为,如何绕开,绕开的关键还是你的用户需要什么,不需要什么。

      从您提到的微薄场景来看,我个人以为,是允许不同人看到不同时序的,没有那么强的一致性。

    23. alex

      一个比较笨的方法,请博主来拍砖

      前提:允许用户看到的内容顺序不一致,但保证用户看到的内容不会缺失。假设ID是整数类型

      采用惩罚机制,来的越晚越往后站

      进入队列数据属性增加一个惩罚值p; p= -(进入队列数据的ID-(队列末尾数据的ID+1)*惩罚系数k) (假设k=10)

      取数据:(cursor*2)<(ID+p)

    Leave a Comment