菜菜哥,你换形象啦?
这么巧,你也换啦!听说是不会画画的菜嫂经过九牛二虎之力的功劳哦!鼓掌......
前几天我出去面试了,面试官问我微服务的知识,我回答的可好了
看来微服务你真的下功夫研究了呀
是呀是呀,但是碰到一个问题,有状态的服务是什么意思呢?
看来你又挂在这个问题上了,且听这次分解
简介
对于初学者,心里对“有状态服务”的理解可能比较模糊,但是从面向对象编程思想的角度去理解也许会明朗很多。面向对象编程思想提倡的是用编程语言去描述世间万物,所以面向对象编程的语言都会提供描述对象的容器以及对象行为的表达方式。举一个很简单的栗子,在c#或者java中,表达对象的容器就是class,对象的行为通过一系列的接口或者函数来表达。更进一步,对象抽象出来之后,大多数对象都有自己的内部状态,体现到代码上也就是常见的类的属性。
面向对象编程的基本思想本质上是对现实世界的一种抽象,万物皆可抽象。
根据业务把对象抽象出来之后,每一个实例化的对象其实都可以有自己的状态,比如:在最常见的游戏场景中,每一个玩家都是“玩家"这类对象的一个实例,每一个玩家都有自己的名字,性别,等级,HP等属性,这些属性本质上就是玩家的状态,随着时间的推移,每个玩家的HP,等级等属性会随之变化,这些变化其实就是这个玩家状态的变化。对应到有状态的服务也是如此,之所以称之为有状态,是因为服务内部的对象状态会随着业务有着对应的变动,而这些变动只发生在这个服务内部,在外界看来,这个服务好像是有状态的。
有状态的服务本质上是一些有状态对象的集合,这些对象状态的变化只发生在当前服务进程中。
优势和劣势
有状态服务之所以被称为有状态,一个很大的原因是它可以追溯状态的变化过程,也就是说一个有状态的服务保存着状态变化的记录,并可以根据这些历史记录恢复到指定的状态,这在很多场景下非常有用。举一个很简单的栗子:我们平时玩的斗地主游戏,三个玩家,当有一个玩家因为网络原因掉线,经过一段时间,这个玩家又重新上线,需要根据某些记录来恢复玩家掉线期间系统自动出牌的记录,这些出牌记录在这个业务中其实就是这个玩家的状态变化记录。在有状态的服务中,很容易做到这一点。
其实实际开发中很多场景不需要记录每个状态的变化,只保留最新状态即可,不单单是因为保存每个状态的变化需要大量的存储和架构设计,更因为是很多业务根本不需要这些状态变化记录,业务需要的只是最新的状态,所以大部分有状态的服务只保存着最新的状态。
有状态的服务在设计难度上比无状态的服务要大很多,不仅仅是因为开发设计人员需要更好的抽象能力,更多的是一致性的设计问题。现代的分布式系统,都是由多个服务器组成一个集群来对外提供服务,当一个对象在服务器A产生之后,如果请求被分配到了服务器B上,这种情况下有状态的服务毫无意义,为什么呢?当一个相同的业务对象存在于不同的服务器上的时候,本质上就违背了现实世界的规则,你能说一个人,即出生在中国,又出生在美国吗? 所以有状态的服务对于一致性问题有着天然的要求,这种思想和微服务设计理想不谋而合,举个栗子:一个用户信息的服务,对外提供查询修改能力,凡是用户信息的业务必须通过这个服务来实现。同理,一个对象状态的查询修改以及这个对象的行为,必须由这个对象的服务来完成。
有状态的服务要求相同业务对象的请求必须被路由到同一个服务进程。
因此,有状态的服务对于同一个对象的横向扩容是做不到的,就算是做的到,多个相同对象之间的状态同步工作也必然会花费更多的资源。在很多场景下,有状态的服务要注意热点问题,例如最常见的秒杀,这里并非是说有状态服务不适合大并发的场景,反而在高并发的场景下,有状态的服务往往表现的比无状态服务更加出色。
Actor模型
在众多的并发模型中,最适合有状态服务设计的莫过于Actor模型了,如果你对actor模型还不熟悉,可以撸一遍菜菜之前的文章:https://mp.weixin.qq.com/s/eEiypRysw5jsC7iYUp_yAg actor模型天生就具备了一致性这种特点,让我们在对业务进行抽象的时候,不必考虑一致性的问题,而且每一个请求都是异步模式,在对象内部修改对象的状态不必加锁,这在传统的架构中是做不到的。
基于actor模型,系统设计的难点在于抽象业务模型,一旦业务模型稳定,我们完全可以用内存方式来保存对象状态(也可以定时去持久化),内存方式比用其他网络存储(例如redis)要快上几个量级,菜菜也有一篇文章大家可以去撸一下:https://mp.weixin.qq.com/s/6YL3SnSriKEnpCyB5qkk0g ,既满足了一致性,又可以利用进程内对象状态来应对高并发业务场景,何乐而不为呢?
有不少同学问过我,actor模型要避免出现热点问题,就算有内存状态为其加速,那并发数还是超过actor的处理能力怎么办呢? 其实和传统做法类似,所有的高并发系统设计无非就是“分”一个字,无论是简单的负载均衡,还是复杂的分库分表策略,都是分治的一种体现。一台服务器不够,我就上十台,百台.....
所有的高并发系统设计都是基于分治思想,把每一台服务器的能力发挥到极致,难度最大的还是其中的调度算法。
用actor模型来应对高并发,我们可以采用读写分离的思想,主actor负责写请求,并利用某种通信机制把状态的变化通知到多个从actor,从actor负责对外的读请求,这个DB的读写分离思想一致,其中最难的当属actor的状态同步问题了,解决问题的方式千百种,总有一种适合你,欢迎你留言写下你认为最好的解决方案。
案例(玩家信息服务)
由于菜菜是c#出身,对c#的Actor服务框架Orleans比较熟悉,这里就以Orleans为例,其他语言的coder不要见怪,Orleans是一个非常优秀的Actor模型框架,而且支持最新的netcore 3.0版本,地址为:https://github.com/dotnet/orleans 有兴趣的同学可以去看一下,而且分布式事物已经出正式版,非常给力。其他语言的也非常出色java:https://github.com/akka/akka
golang:https://github.com/AsynkronIT/protoactor-go
1. 首先我们定义玩家的状态信息
//玩家的信息,其实也就是玩家的状态信息 public class Player { /// <summary> /// 玩家id,同时也是玩家这个服务的主键 /// </summary> public long Id { get; set; } /// <summary> /// 玩家姓名 /// </summary> public string Name { get; set; } /// <summary> /// 玩家等级 /// </summary> public int Level { get; set; } }
2. 接下来定义玩家的服务接口
/// <summary> /// 玩家的服务接口 /// </summary> interface IPlayerService: Orleans.IGrainWithIntegerKey { //获取玩家名称 Task<string> GetName(); //获取玩家等级 Task<int> GetLevel(); //设置玩家等级,这个操作会改变玩家的状态 Task<int> SetLevel(int newLevel); }
3. 接下来实现玩家服务的接口
public class PlayerService : Grain, IPlayerService { //这里可以用玩家的信息来代表玩家的状态信息,而且这个状态信息又充当了进程内缓存的作用 Player playerInfo; public async Task<int> GetLevel() { return (await LoadPlayer()).Level; } public async Task<string> GetName() { return (await LoadPlayer()).Name; } public async Task<int> SetLevel(int newLevel) { var playerInfo =await LoadPlayer(); if (playerInfo != null) { //先进行数据库的更新,然后在更新缓存的状态, 进程内缓存更新失败的几率几乎为0 playerInfo.Level = newLevel; } return 1; } private async Task< Player> LoadPlayer() { if (playerInfo == null) { var id = this.GetPrimaryKeyLong(); //这里模拟的信息,真实环境完全可以从持久化设备进行读取 playerInfo= new Player() { Id = id, Name = "玩家姓名", Level = 1 }; } return playerInfo; } }
以上只是一个简单案例,有状态的服务还有更多的设计方案,以上只供参考完