crazy-bit的博客

crazy-bit的宅基地

0%

最近读完了凯茜-霍姆斯的《时间贫困》,起初是被它的书名吸引的,读完后,却发现收益匪浅,所以想花点时间记录一下,也给大家分享一本好书。
工作的这几年,特别是作为主程的这几年,我一直被一个问题困扰着:时间焦虑。
每天需要做的事情太多,想做的事情也太多,每天忙于各种会议、讨论、编码,想做的事情(比如写blog)一直被推迟,每当静下心来回顾,就会发现这段时间只是一直在工作,个人能力陷入了停滞,这种焦虑感越发严重,心情也越发浮燥,需要长时间耕耘的事情更加无心坚持,做每件事,都仿佛有一个声音在催促你,没时间啦。周末刷会视频,就会有一种负罪感,因为想做的事情这周还毫无进展,而一旦准备做点什么,又会被这种焦虑感弄得心浮气燥,最终草草了事。
如果你在生活、工作中,也有上述困扰,那么这本书也会帮助到你。

作者通过大量调研,给出了时间贫困导致的一系列不良后果:

  • 健康堪忧

    如果时间一直不够用,那我们必然要做出抉择,放弃一些事情,而往往那些当前不紧急、短期影响小的就会出现在放弃名单的顶部,而运动往往是最容易放弃的,因为它不紧急且短时间不会有任何影响,所以当你一天忙忙碌碌,你大概率会减少运动的时间,这也就造成越忙碌,身体越差,效率降低,被迫更加忙碌的死亡螺旋。

  • 待人冷漠

    而另一个最容易放弃的选项就是社交,这也是为什么社会越来越冷漠的原因之一,如果我自己的时间都不够用,那我更不愿意为其他人花费时间,我会减少和朋友之前的聚餐、和好友之间的电话,尽量将自己孤立,避免各种社会交际浪费的时间,特别是在一些高强度的工作者中,比如程序员、外卖员,他们的生活圈会越来越小,因为工作已经占据了他们的大部分时间。

  • 缺乏信心

    当人们处于时间焦虑时,会就对自己缺乏信心,你可以做一个很简单的测试,一项对你来说稍有压力的任务,如果距离交付还有1个月,你有多大把握完成,如果距离交付还有1周,你有多大把握完成。往往时间剩的越多,你就会更有信心,当时间不够时,你可能还会降低质量来回避完不成的消极结果。再举个例子,如果1月初规划情人节礼物,你可能会想着送最浪漫的礼物,但如果是2月13日才去挑选礼物,那你可能就只是想挑一份不失体面的礼物。

好吧,这三点正好都命中我了(精准暴击)。

面对时间贫困的问题,作者也给出了一些方法,我试着一一进行了尝试,总结了一下实践下来对我卓有成效的方法。

缓解焦虑

首先很多人会为每天的时间不够用而焦虑,会因为花时间工作、玩游戏、陪朋友而不能完成计划的事情而懊悔(虽然你可能空闲在家里也只是刷一天手机而已-_-),经常抱怨太忙了,根本连休息的时间就没有。
首先,不要对时间太少而太过焦虑,其实你根据不需要那么多自由的时间。作者通过大量的调研,得出的结论是每天可支配时间在2-5小时之间是最合理的。时间太少,不足以完成任何一件事情,时间太多,会让人失去目标感,所以,在繁忙的每天工作中,能挤出2小时自由支配的时间就够了。而对于8小时工作制的大部分人,这个时间是绝对可以保障的,所以,不需要焦虑时间太少的问题。甚至,你不需要每天都抽出2小时,你一周能抽出一个2小时就足够了。

你需要做的,是找出那些当浪费掉的时间,然后回收回来,书中建议大家做一个每天时间花费的记录,你会发现自己时间消耗在哪里了,当然,对于大多数人(比如说我),不用这么麻烦做到如此精细化,稍微回忆一下,就知道了,回忆一下每天非工作时间在干什么,对于我而言,稍加统计,就可以发现实际我的每日可支配时间接近4小时(要知道,我可以是一名以996著称的程序员),只是大部分时间用来继续工作,或者刷手机了,所以戒掉这两者,我的每日可支配时间就十分可观了。

当你有了可支配时间后,你就可以继续下一步了。

寻找幸福感

什么是幸福感,这里的定义很重要,这个幸福感应该是会带来精神愉悦的,令你充满期待的,给予你动力的,你可以通过做这件事情获得快乐。
这里要和纯感观的快乐区分开来,比如你刷了一小时短视频,你也会很快乐(至少在刷的时候),但放下手机,你会感觉到疲惫、感觉到乏力,并且因为短视频都是包装过的生活,你可能会感觉到更焦虑,更渴望成功,总之,它很难带给你正向的价值,虽然,在最初的几分钟,你确实获得了身心的放松,但因为短视频的成瘾性,你大概率会刷几个小时,在最初几分钟后,你获得的都是负面的情绪价值。
这里要和单纯的成就感区分开来,比如你花一小时划掉了待办清单上的3件事情,在划掉的一刹那,你获得了完成的成就感,但你还要再深刻体会一下,你是否会期待再做一次这3件事情,这3件事情有没有带给你精神上的快乐,如果没有,那就不要把自己变成TodoLister,不断的勾勾勾,这只是一个奶头乐陷阱。
其实判断带来幸福感的事情很简单,你是否会期待它的再次来临。比如我,我十分期待每周和朋友一起打球,为此,我会提前规划好工作,打球这件事情,对我而言,就是带来幸福感的事情,是我的快乐源泉。

重视精神能量

关于幸福感,根据作者的大量调研,得出的结果可能会大大出乎你的意料。
最另人幸福的事情关键词:户外、亲密的人
最不幸福的事情:工作、通勤、家务
是的,不用怀疑,绝大多数人都不喜欢工作,工作就是我们不得已而为之的事情,它是消耗我们的快乐,而不会增长我们的快乐。为自己喜欢的事情工作,相信我,在这个社会分工下,只有极少数人能从事他喜欢的行业,而在其中只有极少数人能在行为中做他喜欢的事情,而又因为各种原因只有极少数人能坚持下来,所以,如果你厌倦工作,不需要PUA自己,这是全球绝大数人的状态。
但工作不能带来快乐,并不意味着我们就不应该工作(相反,工作是大数人生存的依赖,不仅要干,还要干好),我们反而要依靠对快乐的认识缓解我们工作的疲惫,增加工作的效率。
不知道你有没有这样的感受,当你有所期待时,你会更有活力,更有动力,更能应付各种糟心的事情,”盼头”这个东西其实就是我们行动的动力,比如很多人是为了家庭能过上更好的生活而努力工作,很多人是为了能出去看更多的世界而忍受老板的喋喋不休,很多人是为了和朋友的聚餐而高效完成了一天的工作……当你有所期盼时,你也会处于更加兴奋的状态,也更加专注。
但我们在忙碌生活中往往本末倒置了,我们会因为要加班,不和家人共度温心的午后时光,推掉计划好的出游,放了朋友聚餐的鸽子,借口总是”工作太忙了”,”我还不是为了XX”,但其实这可以有双赢的结局,比如花一个小时和爱人散步,并不会担误太多的时间,相反,你的身心得到了放松,就像行驶中停下来加了油,后面你工作更有效率了,反而节约了远远不只一个小时,而且下周工作时,你又会期待下一次的散步,从而让你一周都充满活力。
所以,不要轻视这些让你感到幸福的事情,它们会让你积攒精神能量,从而应对接下来各种不快乐的事情,如果你的工作越忙碌,那你越要抽出时间积攒精神能量,要不然你的工作效率也会越来越低。

付诸实践

当你有了可支配的时间,又知道了能带来幸福感的事情,接下来,就是保障这些可支配时间,尽量应用在这些带来幸福感的事情上。
作者给出一个每周计划表,可以很好的帮助你找到自己快乐的源泉,建议你可以试一下。

填表时,你要回忆下,每周你真正在期待的是什么,每周做完什么事情你获得了最大的满足感,这些期待可以是和朋友约的一顿聚餐,可以是和爱人的一次散步,可以是读了几页书,总之,你要找到自己的幸福的源泉,这点十分重要。上面是我的一个示例,我把这一周最另我开心的事情用黄色标注了。 有了这张计划表,你要做的就是尽量保证这些带来幸福感的事情时间投入,缓解时间贫困的最优解就是将时间用在最重要的事情上,这样你才不会焦虑。

在现代游戏开发中,尤其微服务盛行,一个请求需要经历多个服务处理,不同服务之间也需要高频交互,服务间路由方式越来越多,扩展性和可靠性要求越来越高,需要选择合适的负载均衡方式,来达成系统高可用、可扩展性。下面将介绍几种常见的路由方式,当然,还有更多的负载均衡方式,如Weighted-Round-Robin、Power of Two Choices。
因为介绍负载均衡的文章很多,这里对相关概念都不再详述,主要结合具体的游戏场景来作说明。

一致性哈希(Consistent Hashing)

概述

一致性哈希是一种高效的分布式路由方式,特别适合动态变化的环境。在一致性哈希中,整个哈希空间被视为一个环,节点和数据项都通过哈希函数映射到这个环上。当节点增加或减少时,只有少量的数据需要重新分配,通常是顺时针方向上的数据项。这种特性使得一致性哈希在大规模在线游戏中非常受欢迎,能够有效地减少数据迁移,提高系统的可扩展性。

游戏应用场景

在微服务模式下,ConsistenHash可以说是游戏中应用最多的负载均衡方式,因为它对业务完全透明、扩缩容时流量切换最小,可以极大地简化业务逻辑,提高服务的稳定性,只要可以,请优先使用ConsistentHash,它将大大减少你后续为扩缩容、容灾所做的工作。关于ConsistentHash,你可以参阅更多的文章,这里就不再详述了。
像我之前项目,用的是Hash的方式(参考下文的Shard方式),在机器扩容器时,会产出大量的告警,有时还会发生玩家数据的回退(同一个玩家的请求路由到了两台机器上),改用ConsistenHash后,这种概率就大大降低了,但只要流量发生切换,就会有双写的可能,这里需要其它方式来解决(比如版本号机制),不是负载均衡能解决的。
在使用ConsistentHash中,要注意几个问题:

  • 均匀性
    在使用ConsistenHash后,你会发现流量并不是完全均衡地打到各个节点的,这是正常的,负载最高的节点和最低的节点,负载差距可能会达到30%。
这里的均匀性受节点数量、虚拟节点数量、路由key的数量、hash算法等的影响。 节点数量:节点数量太少,那就只能扩大虚拟节点的数量 虚拟节点数量:虚拟节点越多,会越均衡,但并不是越多越好,因为节点越多,hash环的更新也会更耗时,一般100-200就足够了 路由key数量:像路由key的数量不可以太少,比如用小区ID作为路由key,就会遇到负载极度不均匀的情况,因为小区ID的区间可能也就是百量级,这种就需要引入多key,来加大key的分布范围
  • 性能
    使用ConsistenHash时,要注意hash的Add/Del操作的性能,从下表可以看出,在1000个节点且配置100个虚拟节点的情况下,1000个节点添加完,需要6.4s。这个耗时是很致命的,比如一次故障影响了100个节点,这里完成这100个节点的hash变更会耗时0.64s,直接把服务卡住了。
更好的做法是,不会实时去更新hash环,而是定期批量更新一次,比如每10ms更新一次,这样每次只会消耗单次的耗时,也不影响实时性。

最少负载(Least Load)

概述

最少负载路由策略将请求分发给当前负载最小的服务器。这种方法通过监控每个节点的负载情况,确保请求被均匀分配,从而避免某些节点过载而其他节点空闲的情况。对于需要实时响应的在线游戏,最少负载策略能够有效提高系统的响应速度和用户体验。

游戏应用场景

游戏中有很多服务的业务逻辑很重,比如游戏服务(gamesvr)、单局服务(gameplay/ds),每分配一个玩家或一个单局都会占用大量的服务资源,如果用ConsistentHash,就可能出现部分节点过载的情况,因为这些服务的承载大多只在百或千量级,所以它们的负载需要精心的维护。
经典的LeastLoad业务模型如下,每个节点都需要上报自己的负载信息,可以是承载的单局数、CPU、内存等,上报可以通过定时上报,也可以通过zk/etcd/db等,然后路由层维护所有节点的负载信息,从而进行节点的分配。

但实际游戏业务中,并不会这样,而是增加一个单独的服务来进行节点的负载管理和分配,下面以DS(运行gameplay逻辑)为例:
可以看到,没有使用通过的路由层来实现,而是额外多加了一个ds mgr服务,这样做的原因是,因为DS的承载量低,所以相应的负载分配算法也相对复杂,并且会和业务强耦合,不适合写在通用的路由层,这是由它的复杂性决定的。抽成单独的服务,可以更加灵活的进行定制,比如加入数据预测来更好的分配ds节点。 所以,实际在游戏业务中,LeastLoad的方式有应用,但大都不是走通用实现,而是定制化实现,因为可以走通用实现的大都模型比较简单,相应的负载很高,反而可以用ConsistenHash来代替。

主从(Master-Slave)

概述

主从路由方式通常用于数据存储和管理。在这种模式下,主节点负责处理所有的写请求,而从节点则负责读取请求。主从模式能够有效地分担读取压力,提高系统的读性能。在游戏中,主从模式常用于管理玩家数据、游戏状态等信息,确保数据的一致性和高可用性。

游戏应用场景

游戏中主从的应用场景不多,应该尽量避免去使用主从方式,因为整体方案会比ConsistenHash要复杂。但可能出现某些服务必需是单节点,因为它需要维护全局的状态才能做决绝,那么,针对这种情况,主从是适合的路由方式。

随机(Random)

概述

随机路由是一种简单而有效的负载均衡策略。它将请求随机分配给可用的服务器。这种方法实现简单,适合于负载相对均匀的场景。然而,在负载不均的情况下,随机路由可能导致某些节点过载,而其他节点则处于闲置状态。因此,随机路由通常适用于负载较轻或对延迟要求不高的游戏场景。

游戏应用场景

Random应用场景不多,因为所有可以使用Random的地方,也一定可以使用ConsistenHash,这样,项目就可以少维护一种负载均衡方式了。

分片(Shard)

概述

分片是一种将数据分割成多个部分并分配到不同节点的路由方式。在游戏中,分片可以根据玩家的地理位置、游戏角色或其他属性将玩家请求分配到特定的服务器。这种方法能够有效地减少延迟,提高玩家的游戏体验。分片策略通常与一致性哈希结合使用,以实现更好的负载均衡和数据管理。
经典的Shard路由方式如下:

游戏应用场景

为什么有了ConsistenHash后,还会需要Shard这种方式的?(相对于ConsistentHash,Shard方式在节点挂掉时,即不能保证服务可用性,也需要额外配置每个节点的分配的key)但对于一些强缓存的服务,Shard的方式反而更加简单。
如非必须,不要使用Shard的路由方式,因为它会带来维护的复杂性,需要考虑key的分配,需要考虑扩缩容时数据的迁移。
游戏中的搜索是一个典型的适合使用Shard的服务。比如某游戏的搜索服务,提供玩家众多的选项来筛选自己心怡加入的队伍一起作战,这时就需要一个搜索服务来缓存所有的队伍,并按各种条件建索引(当然,你也可以用ES组件),它的业务模型是这样的(假设数据需要N个节点来承载):

每个索引服务存储了部分的队伍数据,所有索引服务的并集是所有的队伍数据,玩家每次搜索,就是去每个索引服务上搜索符合条件的队伍,在接入层进行结果汇总,并返回给玩家。
采用Shard的方式,每个节点很容易判断数据是不是自己负责的,各个节点启动时可以直接从DB加载需要的数据,完成冷启动,并且当某个节点挂掉后,对于搜索服务来说,它只是有损的(搜索结果变少了),玩家甚至感受不了服务出现了异常,而当这个服务拉起后,走冷启动就可以恢复数据。基于这个前提,你可以通过简单的重启服务完成扩缩容,故障处理也不需要太实时,走正常故障恢复就可以。

轮询(Round Robin)

概述

轮询是一种简单的负载均衡策略,它将请求依次分配给每个可用的服务器。轮询方法实现简单,能够确保请求的均匀分配。然而,在节点负载不均的情况下,轮询可能无法有效地平衡负载。因此,轮询通常适用于负载相对均匀的场景。

游戏应用场景

轮询在游戏中应用场景不多,在Web类应用比较多,游戏业务大部分都是强状态的,一些对外的服务适合使用这种负载均衡方式,比如gatesvr(提供http相关的服务), dirsvr(提供客户端的接入)等。所有对外的服务,都可以采用这种方式。

点对点(P2P)

概述

点对点(P2P)路由方式允许玩家之间直接通信,而不需要通过中心服务器。这种方法能够减少延迟,提高数据传输速度,适合于需要实时交互的在线游戏。P2P 网络的一个典型应用是多人在线游戏中的玩家对战,玩家可以直接连接到其他玩家的客户端进行游戏。

游戏应用场景

P2P是游戏中典型的路由方式,比如A玩家邀请B玩家,或者公会向所有成员推送消息等,P2P需要额外的记录地址信息,用于消息发送的目的地址,可以记录在一张online表中,也可以记录在公会成员的信息中。

广播(Broadcast)

概述

广播是一种将请求发送到所有可用节点的路由方式。在游戏中,广播可以用于发送全局消息、更新游戏状态或进行事件通知。虽然广播能够确保所有节点都接收到信息,但在节点数量较多时,可能会导致网络拥塞和性能下降。因此,广播通常适用于需要全局同步的场景。

游戏应用场景

Broadcast也是游戏中典型的路由方式,也P2P相反,它是发送给所有的目标,在游戏中的应用场景也很多,比如广播公会的所有成员、广播所有的dirsvr当前gamesvr的负载(假设dirsvr是用来管理gamesvr负载的服务)等。
这里的广播也通常意义的广播还稍有区别,像很多游戏都是分区的(不同的帐号体系不同的大区),分小区的、分平台的,所以广播也相应有不同的级别。

  • 全区广播:发送给所有的指定服务类型节点
  • 大区广播:发送给本大区所有的指定服务类型节点
  • 小区广播:发送给本小区所有的指定服务类型节点
  • 有限广播:发送给指定的节点列表

像上文例子中广播公会的所有成员,一般游戏的实现可能并不是广播,因为每个成员所在的节点不同,实际实现会变成发送N次P2P消息;当然,更好的是支持有限广播,通过指定要发送的列表,由路由层进行消息的分发,而不是由业务层进行。
在进行广播时,需要注意的是广播风暴的问题,这种一般出现在多级路由时,如果路由层一味地将消息广播给所有周边节点,那就会如图所示进入死循环,形成广播风暴,解决它也很简单,广播时踢除掉来源的链路就可以了。

但是如果我们稍微复杂一点,就会发现踢除来源链路,解决不了下面的问题,节点X还是会广播两遍,这里就需要进一步对消息进行标识,踢除掉已经广播过的消息。除此之外,消息中最好带上TTL,每经过一跳就+1,当达到X跳时,这条消息自动丢弃,这样可以杜绝广播风暴的产生。

有状态服务

有状态服务在游戏行为应用较多,早期的游戏服务基本都是有状态的服务,这样的服务写起来最为简单、性能也高,这也是早期手游爆发期,业务快速上线需要与服务稳定性的折衷;但随着手游市场的竞争加剧,品质要求也越来越高,有状态服务对于故障、扩缩容的处理也越发复杂。

按数据缓存时机划分

根据服务数据缓存的时机,可以分为启动时加载与LRU按需加载

启动时加载

服务启动时,即加载完所有需要缓存的数据,这种适用于数据需要冷启动的服务,如队伍搜索这样的服务,冷启动的数据来源一般来自数据库或配置生成,数据量集一般较大,需要一定的加载时间,在运行过程中通过增量的方式继续保证数据的有效性。

按需加载

服务启动时,不进行任何数据加载,而是根据收到的请求(校验通过后),进行数据的加载并缓存,缓存的数据会长期存在,因为服务的内存是有限的,一般会设置缓存数据的上限,超出上限的会进行拒绝服务(如gamesvr的player缓存)或淘汰旧的缓存(如mailsvr的mailhead缓存)。

按数据路由方式划分

根据数据缓存的方式,决定了访问数据的不同路由方式,一般分为三种:ConsistentHash、Shard、Stand-alone。

ConsistentHash

一般用于按需加载的方式,根据访问的key决定了路由到的服务实例,通常情况下,对于相同的key,路由到的服务实例是相同的;在服务故障、扩缩容等情况下,相同的key的前后两次访问可能路由到不同的服务实例,所以需要处理数据迁移、双写等问题。
e.g. RankboardSvr按RankboardKey进行ConsistentHash路由

Shard

一般用于启动加载的方式,相同的key,无论何种情况,都路由到相同的服务实例,这样保证了路由的稳定性,但服务实例不可能一直保持健康,所以需要处理服务可用性等问题。
e.g. CacheSvr用于队伍数据的搜索

Stand-alone

单点缓存服务,一般用于可用性要求不高的业务场景,在业务中用处不多,一般还是要考虑至少主备的方式进行容灾。
e.g. GlobalSvr用于全局序号器生成(已废弃)

读写操作

有状态服务可以从读操作、写操作来进行分析。

读操作

内存操作,不访问DB,性能高,消耗小;按需加载方式时,如果内存中没有缓存,还需要先建立缓存。

写操作

按写入的时机不同,可以分为定时写与即时写两种。

  • 定时写
    写操作只操作内存,不对DB进行操作,性能高,消耗小。缺点是数据回写前会有丢失的风险。
    e.g. GameSvr中player数据的回写
  • 即时写
    写操作同时操作内存和DB,性能较低,好处是没有数据丢失风险。

无状态服务

因为有状态服务运营过程中存在着诸多不便,无状态服务重新进入游戏开发的视野(要理解这个过程不是一蹴而就的,依赖的各种技术及实现方案是随着一代代产品逐步完善的),并随着云服务的兴起,无状态服务以其独特的优势成为服务的首选。

无状态服务

它们的共同特点是本身不缓存任何数据,故障或重启对业务的影响微乎其微, 只需要简单的容灾方案,服务的可用性就可以大幅提升。业务框架也因此变得极为通用,可以很简单的抽象出一套开发框架,大大简化开发门槛、提升开发质量。
根据服务是否有需要访问其它数据,又可以分为转发服务与Web型服务。

  • 转发服务
    典型的转发服务:proxysvr、gatesvr。
    路由可以使用Random路由,扩缩容和故障对业务几乎没有影响。

  • Web型服务
    典型的数据处理服务:idipsvr、teamsvr。
    路由需要采用ConsistenHash路由,扩缩容和故障需要考虑短暂的双写问题,但即使不处理,对业务的影响也极小。

但这不是本文的重点,因为数据库压力、性能等考虑,无状态服务是比较有局限性的,而无状态和有状态的中间体:弱状态服务就显现出其优势。

弱状态服务

“弱状态”是一个新造的词汇,表示服务是有状态的,但又具有无状态服务的特点(简单扩缩容、容灾等)。
典型的弱状态服务:friendsvr(管理好友)、guildsvr(管理公会)、rankboardsvr(管理排行榜)。

数据缓存模型

理解”弱状态”服务首先要理解无状态服务在实践过程中遇到的问题:

  • 每次读操作都从DB加载消耗了大量CPU,增加时延,导致服务的性能急剧下降
    以mailsvr为例,假设每个玩家至多有200封邮件,那每次读操作都需要从数据库中拉取这200封邮件,虽然可以做一些设计上的优化,如只拉取邮件头,但仍然是一块不小的开销。
    而在一般的业务中,读量是远大于写量的(10倍+),所以每次读都转换成一次DB操作并不是一个好的设计,反而加一层缓存,能完美解决读的问题。
    所以,弱状态服务的读操作都是直接从内存返回的。

  • 每次写操作造成了DB的压力(部分服务)
    以rankboardsvr为例,排行榜变动是比较频繁的,特别是对于一些大容量的排行榜(10000+),如果每次变动,都进行DB回写,一个是回写的数据量大,需要消耗较多的CPU与IO等待,一个是回写的频率高,会对DB造成极大的压力。
    所以DB的回写可以看作一个很重的操作,需要进行收敛,最简单的做法就是定时回写(e.g.每秒回写一次),这样能完美解决写的问题。
    当然,写量在一般场景下是偏小的,对这种场景,采用即时写是最佳方案。

下面来完整描述下缓存模型
服务对DataX(key=X)进行缓存,缓存仅存活一小段时间,读操作直接从缓存中读取,写操作直接写DB。
缓存时长:假设N毫秒(实践上看设定1秒的缓存时长和回写时长能应对绝大部分场景)。
要充分考虑数据的不一致,缓存时间越长,越可能出现内存与DB数据不一致,导致读取的数据是错误的;对于定时写而言,回写时间越长,越可能出现数据的丢失。
读操作:如果缓存不存在,从DB加载,建立缓存并返回数据;如果缓存存在,直接返回数据。
写操作:在写操作时,从DB加载,修改数据,并回写DB
有些服务(比如rankboardsvr),写操作数量和大小都高,不是每次都进行回写,这里需要用定时回写的方式,如果缓存不存在,从DB加载,建立缓存并修改数据;如果缓存存在,直接修改数据。

路由选择

无状态服务采用最简单的随机路由即可,而弱状态服务则不可以,因为带有状态,需要保证路由的一致性,这里最佳选择就是ConsistentHash。
ConsistenHash只能保证相同的数据的访问,路由到同一台服务实例,因为有异步存在,同一台服务实例的请求处理也会出现并发,这里就需要用本地队列的方式来解决:
本地队列保证了相同数据的访问是串行的,上一个请求完全处理完,才会处理下一个请求,避免了同一个服务实例上并发处理的问题。(当然,这个特性xRPC也是内置的)

扩缩容及容灾

对于无状态服务,扩缩容和容灾就是极其简单的,甚至不用做任何工作;而对于弱状态服务,则需要考虑状态数据的迁移问题。
无论是在扩缩容还是故障时,服务的路由都可能发生变化,这时就会在短暂的时间(100ms左右)出现同一个数据的请求路由到不同的服务实例上,同时处理的情况,对于读操作来说,这没有影响,但考虑到写操作就要考虑读写冲突、多写冲突的问题了。

应对这种场景,有如下的解决方式:

  • “鸵鸟”策略

    业务本身不进行任何处理,任由双写发生就好了,基于多写的情况并不多,及业务身自要求并不高的情况下,这是一种性价比极高的处理方式,实际上过去很多游戏就是这样处理的,并且运行得很好(世界远没有你想的那么不堪)。

  • 带版本号回写

    虽然双写无法解决,但能感知并保证双写结果可预测性也是十分有意义的,最简单的办法就是引入数据版本号,每次回写版本号+1,数据库只接收比当前数据大的版本号回写,这样,当发生双写时,后写的因为版本号会失败,这样业务可以即时告警,后续通过人工干预进行修复,并且,双写的行为也可预期的,后写入的失败。

  • 分布式锁

    当然有很多种方式来解决双写的问题,其中一种就是分布式锁方案。在访问数据前,先需要获取锁,这个锁是全局的,所以在数据处理完之前,不会有其它进程能修改这块数据。
    当然,分布式锁需要考虑的问题也很多,比如续租、锁超时、锁性能等,都和实际采用的锁实现有关(如基于zk,基于redis等),这些问题不在这里详述,这个方案需要考虑到锁服务对性能的影响。

  • 数据迁移(Kickout)

    在版本号的基础上,稍加改进,可以得到一个数据迁移版本。
    利用数据库的作为中心节点(全局服务也可以),记录DataX当前被哪个服务实例持有,当有其它服务实例试图访问这个数据时,需要先将DataX从持有的服务实例踢下线,再锁定此数据DataX。
    DataX需要记录持有的服务实例与持有开始的时间戳(可选)
    持有的服务实例:用来判定DataX目前被谁所持有,如果是自身,那直接使用即可;如果是nil,那需要写入持有信息,再进行使用;如果是其它实例,那需要进行Kickout操作,再写入持有信息
    持有开始的时间戳(可选):在服务实例首次持有DataX时,需要设置时间戳,用于避免短时间内DataX被多个实例反复抢占的情况(默认可以设置为200ms),即如果上一个持有者持有的时间不超过200ms,那么其它人不能进行抢战操作。

    Kickout实现细节:
    A向B发生Kickout请求,需要等待B回复后,才可以进行后续的数据加载和锁定;如果等待B回复超时,A会进行N次重试(默认为3次),N次重试都超时后,A会强行加载数据并锁定;
    B收到Kickout请求后,会检查当前DataX的消息是否已处理完,设置缓存失效,检查队列中是否还有DataX的消息(Kickout本身任何其它操作,需要设置为pipeline),都没有则回复成功,否则回复失败。
    注:Kickout消息需要设置为pipeline的原因

  • 数据迁移(2PC)

    考虑到业务的高可用服务,可以通过两阶段提交的方式进行数据迁移来保证数据无损。

    上面是一个简单的两阶段描述,数据迁移方案很多(比如双写方案、动态拦截方案),这个方案需要考虑实现的复杂度。

抽象弱状态服务模型

根据上文的描述,我们可以得到弱状态服务中,一个RPC处理的简化模型:

其中,只有红框的部分是需要业务实现的,其余部分都可以抽像到框架,这样,你不需要关心数据从哪来的,是否合法,以及前置检查,也不需要关心数据有没有回写,你只需要使用数据执行逻辑就可以了(这部分也涉及RPC的设计,有空再详述)。

“接受父母的平庸 接受自己的平庸 接受孩子的平庸”

回顾毕业这十年,总体来说,我是幸运的,从一个小镇出来的”做题家”,在深圳安了家,有了圆满的家庭,前十年的工作给了我丰厚的回报,事业上也获得了晋升机会,有机会从0组建一个后台团队;但同时,我也是不幸的,项目的轰然倒塌,让我一切又重回了原点,这十年的努力感觉就在这一刻归0。

“飞弛人生2”中有句话说的很好,”我努力过很多次了,但机会,只会出现在这其中的一两次”,当时电影看到这里时,就深深触动了,但直到现在,脑海中,再浮现这句话,我才真正的理解了,有些事情,你没经历过,你就无法共情,无法理解,即使你明白这句话的意思,这句话的道理,这就是知道和经历的区别。对于此刻的我而言,属于我的机会没有了,属于我的骄傲没有了。

凭心而论,这十年,我是足够努力的,我也是足够聪明的(可能达不到顶尖的水平),但这个世界就是如此残酷,这几天彻夜难眠,反复告诉自己要振作,制定了一项项的计划,但你会发现,这些计划做到了又怎样,什么都改变不了,机会就这一次,错过了就没有了,不是你努力就会有的。

“接受自己的平凡”,这是我唯一要做的事情,记得几年前,在一次回老家的山路上,爸爸就语重心长地说过,”人这一辈子都有辉煌的时刻,不要学XXX像了不得的,过好自己的生活就可以了”,我爸的心态也确实是我一直羡慕的,他和我妈是真正做到了平平淡淡的生活,不羡慕别人有钱,也不和朋友计较得失,对父母孝顺,对物质看得很淡然。那次谈话,虽然爸爸说得我都明白,道理我也都懂,但我却共情不了,我就觉得还要换更大的房子,挣更多的钱,”接受自己的平凡”,道理很简单,但有谁又能真正做到。

然而,我又是幸运的,至少在37岁这年,我遇到了这些事,整整一个星期,基本都处于失眠状态,吃饭也没有胃口,一直在思考自己的前途,家庭的重担如何承担下去,这些,又怎么会有答案呢,至少现在我还有工作,至少我现在还能养活我的家庭,那就继续努力得活下去,这一周待在家里,发现”平凡的自己”也没有什么不好,至少能多陪陪家人,人为什么要为了欲望而活着呢!我有这么可受的小孩,这么听话懂事,还有这么善解人意的老婆,即使后面生活档次可能会下降,甚至要搬离深圳,她都很淡然,确实,她一直都很知足,一家人能平淡的生活,她就会很开心了。

所以,我就继续做好家庭的顶梁柱就好了,可能顶得高一点,可能顶得低一点,但我知道,一家人在一起就会很开心。这段经历虽然度过得很艰难,但我相信,每段经历都会是我以后宝贵的财富,所以今后还是要继续努力,即使我的机会已经基本没有了,但我仍要保持继续工作,大头兵也没什么不好,还是要继续保持技术上的精进,做一个能干到50岁的大头兵。

努力吧