现在的位置: 首页 > 综合 > 正文

分布式系统中时序的重要性

2013年09月26日 ⁄ 综合 ⁄ 共 4469字 ⁄ 字号 评论关闭

 

分布式系统中时序的重要性

本文是我们组前2天讨论交易邮件的处理流程过程讨论过程的总结。

交易邮件就是用户在拿取附件的同时,必须付给发件人指定的相应的报酬的邮件。

由于Pets是一个全区全服的游戏系统系统,邮件服务器可能有多台,不同的用户的邮件存放在不同的邮件服务器Mailsvrd 上。

假设的场景都是用户B发送交易邮件给用户A,用户A如果同意交易,则在拿取附件的同时,要按照用户B的要求数量给用户B发送一封付款(Pets的货币名称是元宝)邮件。用户A的邮件数据在邮件服务器Mailsvrd A上,用户B的邮件在邮件服务器Mailsvrd B上。另外客户端不直接链接Mailsvrd,而是通过接入层的Fedsvrd进行游戏。

                                                                                                                                                                图1 邮件系统架构

 

1               “正常”思维的方案

最开始的方案如下图,这是一个典型的“正常”思维的过程,处理的时序思路就是按部就班。

这个过程基本思路就是客户端在请求拿取附件后,前端的养成服务器负责处理整个流程,和后面的2Mailsvrd服务器交互完成处理。

                                                                                                                                                       图2 正常的拿取邮件请求

如果用户正常,这个事情也没有任何问题。但是……,中国的用户都是不正常的。假如用户破解客户端或者直接修改协议进行发送,(Sniffer这类工具都有这类功能)。那么用户可能在第一次请求拿取附件后,再发送一条拿取附件请求。如下图,注意中间的那条蓝色的线。

 

                                                                                                                                                 图3 用户攻击导致服务器异常

这个请求,在服务器上一般会新开一个处理单元(线程或者事务处理对象)对这个请求进行处理。那么很可能(触发你有复杂的保护代码和回滚逻辑)导致一个结果是可以多次获得邮件附件。

2               加入事务锁

发现这个问题后,进行了思路的改变。我们要保证用户在一段时间内只能发起一个这样的事务。

事务锁可以加在前端服务器(面向客户端),但是考虑到前端服务器的有很多个点,锁的控制点放在后面的控制点。

觉得必须在Mailsvrd上增加一个拿取附件的事务,同时对这个事务增加事务锁。

改造后的时序变成了如下:

 

                                                                                                                                                          图4 加入事务锁的时序

 这个方法是在用户A拿取附件请求Mailsvrd服务器的时候,Mailsvrd服务器对这个用户的拿取行为进行加锁。如果用户A再有任何拿取请求都拒绝。

事务锁本身就是一个限制检查,不是阻塞类型。所以对服务器的性能没有影响。

3               如果有天灾人祸

这样的确安全了很多,但是由于是分布式系统,任何一个节点都可能坏掉,天灾人祸是避免不了的,那么假如Mailsvrd B服务器坏掉了呢?

有两种糟糕可能,部分用户倒霉或者部分用户可能得到可以利用的漏洞。

假如你的代码时序就如同加入事务锁的时序,那么Mailsvrd B 请求失败的情况下,用户B将无法得到应得的元宝。另外假如你的时序和前面的方案略有差别,仅仅改变了修改邮件A状态以及给用户B邮件的时序前后关系,结果。结果会如何呢。这要看你如何处理Mailsvrd B返回的失败了。如果你在失败的情况下没有回滚操作,而且没有继续后面的操作到Mailsvrd A上修改邮件的状态,那么就可能导致用户A在这段时间内都能利用这个漏洞反复获得邮件内部的物品。

 

                                                                                                                                                                      图5 天灾人祸

所以在分布式系统的多阶段(可以理解为有限状态机)的处理过程中,一定要考虑超时处理以及错误处处理。后面我们再来慢慢分析这些问题。

4               把危险的操作放在前面

再回头看看2种情况,先到Mailsvrd A服务器上处理修改邮件状态,还是先到Mailsvrd B上发送邮件。这的确是一个问题。

危险的操作步骤放在前面操作,主要的目的是在出现错误后避免处理更多的回滚操作。【注】

 

【注】危险操作步骤尽量放在前面完成,这应该算一个准则,但也要明白的是一切准则都有例外。

 

所以如果为了避免更多的在出现错误后进行复杂的回滚操作。先处理危险的操作是一个比较好的选择。

假设服务器的处理逻辑都正确(一般情况还是应该这样假设吧),那么可以任务,交互第一步骤FEDSVRD已经到Mailsvrd A上取过一次邮件,在这个事务的周期(最不及也只有5s时间吧)内,Mailsvrd A停止服务(coredump,断电)的概率不会很高,大致小于0.0001%吧,就算Mailsvrd A在用户读取邮件后停止了服务,会出现问题的用户大致也只有5s以内,为这5s内出错的用户写回滚语句是否值得全看你的个人意志和观点。个人倾向于逃避这个问题,记录日志便于日后回溯也许就足够了(大家也许好奇,为什么我一直主张逃避复杂的回滚操作,请看下一节)。

但对于服务器B,我们此时无法判断他是否可靠【注】。所以在读取邮件信息操作后,对于Mailsvrd B的操作危险成都远远大于对于Mailsvrd A的操作。

 

【注】一直在思考在分布式系统中的一个问题,有没有方法让一个服务器知道其他服务器的状态?到目前为止我的结论还是未必能实现,而且实现的意义待考。因为服务器的架构往往希望对于很多其他节点屏蔽信息。

综合前面的意见,我们又将服务器的时序调整成了下面这个样子。

 

                                                                                                                                                    图6 危险操作放前面的方案

 

5               回滚,适度就好

首先必须说明,回滚很难。分布式系统的回滚更是难上加难。但是也绝不是完全不做回滚操作。这个也符合tony老大的柔性服务思路。

5.1               为什么回滚那么难

前面这个例子,我们的回滚操作只发生在2个阶段后面,第一个是给用户B发送付款邮件失败,第二个是删除邮件的附件失败。

如果在第一个阶段失败后回滚,我们要回滚删除用户A的道具(前面增加了),回滚增加用户A的元宝(前面扣除了)。

如果在第二个阶段失败后回顾,我们要删除给用户B的邮件,还要要回滚删除用户A的道具,回滚增加用户A的元宝。

如果将前面的处理看做有限状态机,你可以认为在一个阶段上发生问题后,就是要在前的步骤逆向走一次。

是不是够复杂了?还不复杂?如果回滚的步骤中又有一个失败了呢?【注】

 

【注】一般而言,回滚的操作失败的处理方式是继续回滚。

 

所以尽量少回滚,能用时序避免的就避免。在危险操作放前面的方案中,如果认为删除邮件附件的失败几率很小,完全可以不去考虑回滚,因为一旦发生故障,这样影响的用户数量会极少,极少。毕竟我们不是银行业。对于这类故障的处理方式,可以考虑记录本地日志和在日志服务器上记录问题。这样至少保证日后有据可查。

5.2               完全不回滚?找死

这个观点几乎也不用解释,涉及钱,用户财富的事情,如果能回滚还是有必要进行回滚操作的。

5.3               容易回滚的步骤先处理

还是前面的哪个例子,如果我们的方案已经是危险操作放前面的方案。只在第一个阶段给用户B发送付款邮件失败后进行回滚,那么回滚操作只会有两个,回滚增加用户的元宝,回滚扣除用户获得的物品。

一般而言,用户的元宝比较容易回滚,而背包操作的回滚就要复杂一些,完整的回滚方案要么是记录原来放入的位置才能正确回滚,或者要采用加锁,再记录用户原有背包数据便于在回滚中使用这样的方式。

再回头看原来时序,你可以发现其实发送邮件前,只需要先扣除用户A的元宝就可以继续给用户B发送邮件,不用先给用户A邮件附件。这样我们可以进一步改进方案。将用户A拿取附件的时序放在给用户B发送邮件后面。

这样如果给用户B发送邮件失败后,只需要回滚增加用户A的元宝。这大致是我们最终的方案。

 

                                                                                                                                                    图6 容易回滚的步骤先处理

 

5.4               如何写回滚

前面以及说过了,最简单的方法一般都是加锁,然后记录下原有的数据,再进行操作,回滚时利用原有的数据恢复数据区,最后解锁。【注】

 

【注】比较麻烦的是Pets有一些操作是服务器自动给(扣)用户物品,用户不用主动操作,此时加锁也会引发一些麻烦。我认为这是一些设计导致的结构复杂性。作为程序很难解决这类设计导致的结构化矛盾。

另外,对于用户的请求,处理的模型是采用有限状态机(或者说是事务)的模型,这样在回滚的过程中,可以根据这个所处的状态进行回滚。

6               总结

 

分布式系统的开发过程中,由于往往要修改N个点的数据才能完成操作,事务性很难保证,要仔细实考各个某个操作的各个步骤的时序。

分布式系统的是一个面向异常的系统

危险的步骤一般会放在前面,这样可以避免出现更多的回滚操作。

 

谨慎面对回滚类的操作,

 

抱歉!评论已关闭.