前言...........

上一篇文章我们讲了Akka Remote,理解了Akka中的远程通信,其实Akka Cluster可以看成Akka Remote的扩展,由原来的两点变成由多点组成的通信网络,这种模式相信大家都很了解,就是集群,它的优势主要有两点:系统伸缩性高,容错性更好。

集群概念

很多人很容易把分布式和集群的概念搞错,包括我也是,我一开始也以为它们两个是一样的概念,只是叫法不同而已,但其实不然,虽然它们在实际场景中都是部署在不同的机器上,但它们所提供的功能并不是一样的。举个简单的例子来看看它们之间的不同:

为了保持整个系列连续性,我又以抽奖为基础举一个例子:

假定我们现在抽奖流程包括,抽奖分配奖品和用户根据链接领取指定奖品,用户先抽奖然后获取奖品链接,点击链接填写相应信息领取奖品。

1.分布式:

我们现在把抽奖分配奖品和用户根据链接领取指定奖品分别部署在两台机器上,突然有一天很不幸,抽奖活动进行到一半,抽奖分配奖品那台机子所在的区域停电了,很显然,后续的用户参与抽奖就不能进行了,因为我们只有一台抽奖分配奖品的机子,但由于我们将领取奖品的业务部署在另一台机器上,所以前面那些中奖的用户还是可以正常的领取奖品,具体相关定义可参考《分布式系统概念与设计》中对分布式系统的定义。

2.集群:

现在我们还是有两台机器,但是我们在两个机器上都部署了抽奖分配奖品和用户根据链接领取指定奖品的业务逻辑,突然有一天,有一台所在的区域停电了,但这时我们并担心,因为另一台服务器还是可以正常的运行处理用户的所有请求。

它们的各自特点:

  • 分布式:是指在多台不同的服务器中部署不同的服务模块,通过远程调用协同工作,对外提供服务;
  • 集群:是指在多台不同的服务器中部署相同应用或服务模块,构成一个集群,通过负载均衡设备对外提供服务;

总的来说: 分布式是以分离任务缩短时间来提高效率,而集群是在单位时间内处理更多的任务来提高效率。

Akka Cluster

在前面的文章Akka Actor的工作方式,我们可以将一个任务分解成一个个小任务,然后分配给它的子Actor执行,其实这就可以看成一个小的分布式系统,那么在Akka中,集群又是一种怎样的概念呢?

其实往简单里说,就是一些相同的ActorSystem的组合,它们具有着相同的功能,我们需要执行的任务可以随机的分配到目前可用的ActorSystem上,这点跟Nginx的负载均衡很类似,根据算法和配置将请求转发给运行正常的服务器去,Akka集群的表现形式也是这样,当然它背后的理论基础是基于gossip协议的,目前很多分布式的数据库的数据同步都采用这个协议,有兴趣的同学可以自己去研究研究,只是我也是一知半解,这里就不写了,怕误导了大家。

下面我来讲讲Akka Cluster中比较重要的几个概念:

Seed Nodes

Seed Nodes可以看过是种子节点或者原始节点,它的一个主要作用用于可以自动接收新加入集群的节点的信息,并与之通信,使用方式可以用配置文件或者运行时指定,推荐使用配置文件方式,比如:

  1. akka.cluster.seed-nodes = [
  2. "akka.tcp://ClusterSystem@host1:2552",
  3. "akka.tcp://ClusterSystem@host2:2552"]

seed-nodes列表中的第一个节点会集群启动的时候初始化,而其他节点则是在有需要时再初始化。

当然你也可以不指定seed nodes,但你可以需要手动或者在程序中写相关逻辑让相应的节点加入集群,具体使用方式可参考官方文档。

Cluster Events

Cluster Events字面意思是集群事件,那么这是什么意思呢?其实它代表着是一个节点的各种状态和操作,举个例子,假设你在打一局王者5v5的游戏,那么你可以把十个人看成一个集群,我们每个人都是一个节点,我们的任何操作和状态都能被整个系统捕获到,比如A杀了B、A超神了,A离开了游戏,A重新连接了游戏等等,这些状态和操作在Cluster Events中就相当于节点之于集群,那么它具体是怎么使用的呢?

首先我们必须将节点注册到集群中,或者说节点订阅了某个集群,我们可以这么做:

  1. cluster.subscribe(self, classOf[MemberEvent], classOf[UnreachableMember])

具体代码相关的使用我会再下面写一个demo例子,来说明是如何具体使用它们的。

从上面的代码我们可以看到有一个MemberEvent的概念,这个其实就是每个成员所可能拥有的events,那么一个成员在它的生命周期中有以下的events

  • ClusterEvent.MemberJoined - 新的节点加入集群,此时的状态是Joining;
  • ClusterEvent.MemberUp - 新的节点加入集群,此时的状态是Up;
  • ClusterEvent.MemberExited - 节点正在离开集群,此时的状态是Exiting;
  • ClusterEvent.MemberRemoved - 节点已经离开集群,此时的状态是Removed;
  • ClusterEvent.UnreachableMember - 节点被标记为不可触达;
  • ClusterEvent.ReachableMember - 节点被标记为可触达;

状态说明: - Joining: 加入集群的瞬间状态 - Up: 正常服务状态 - Leaving / Exiting: 正常移出中状态 - Down: 被标记为停机(不再是集群决策的一部分) - Removed: 已从集群中移除

Roles

虽然上面说到集群中的各个节点的功能是一样的,其实并不一定,比如我们将分布式和集群融合到一起,集群中的一部分节点负责接收请求,一部分用于计算,一部分用于数据存储等等,所以Akka Cluster提供了一种Roles的概念,用来表示该节点的功能特性,我们可以在配置文件中指定,比如:

  1. akka.cluster.roles = request
  2. akka.cluster.roles = compute
  3. akka.cluster.roles = store

ClusterClient

ClusterClient是一个集群客户端,主要用于集群外部系统与集群通信,使用它非常方便,我们只需要将集群中的任意指定一个节点作为集群客户端,然后将其注册为一个该集群的接待员,最后我们就可以在外部系统直接与之通信了,使用ClusterClient需要做相应的配置:

  1. akka.extensions = ["akka.cluster.client.ClusterClientReceptionist"]

假设我们现在我一个接待的Actor,叫做frontend,我们就可以这样做:

  1. val frontend = system.actorOf(Props[TransformationFrontend], name = "frontend")
  2. ClusterClientReceptionist(system).registerService(frontend)

Akka Cluster例子

上面讲了集群概念和Akka Cluster中相对重要的概念,下面我们就来写一个Akka Cluster的demo,

demo需求:

线假设需要执行一些相同任务,频率为2s一个,现在我们需要将这些任务分配给Akka集群中的不同节点去执行,这里使用ClusterClient作为集群与外部的通信接口。

首先我们先来定义一些命令:

  1. package sample.cluster.transformation
  2.  
  3. final case class TransformationJob(text: String) // 任务内容
  4. final case class TransformationResult(text: String) // 执行任务结果
  5. final case class JobFailed(reason: String, job: TransformationJob) //任务失败相应原因
  6. case object BackendRegistration // 后台具体执行任务节点注册事件

然后我们实现具体执行任务逻辑的后台节点:

  1. class TransformationBackend extends Actor {
  2.  
  3. val cluster = Cluster(context.system)
  4.  
  5. override def preStart(): Unit = cluster.subscribe(self, classOf[MemberEvent]) //在启动Actor时将该节点订阅到集群中
  6. override def postStop(): Unit = cluster.unsubscribe(self)
  7.  
  8. def receive = {
  9. case TransformationJob(text) => { // 接收任务请求
  10. val result = text.toUpperCase // 任务执行得到结果(将字符串转换为大写)
  11. sender() ! TransformationResult(text.toUpperCase) // 向发送者返回结果
  12. }
  13. case state: CurrentClusterState =>
  14. state.members.filter(_.status == MemberStatus.Up) foreach register // 根据节点状态向集群客户端注册
  15. case MemberUp(m) => register(m) // 将刚处于Up状态的节点向集群客户端注册
  16. }
  17.  
  18. def register(member: Member): Unit = { //将节点注册到集群客户端
  19. context.actorSelection(RootActorPath(member.address) / "user" / "frontend") !
  20. BackendRegistration
  21. }
  22. }

相应节点的配置文件信息,我这里就不贴了,请从相应的源码demo里获取。源码链接

接着我们来实现集群客户端:

  1. class TransformationFrontend extends Actor {
  2.  
  3. var backends = IndexedSeq.empty[ActorRef] //任务后台节点列表
  4. var jobCounter = 0
  5.  
  6. def receive = {
  7. case job: TransformationJob if backends.isEmpty => //目前暂无执行任务节点可用
  8. sender() ! JobFailed("Service unavailable, try again later", job)
  9.  
  10. case job: TransformationJob => //执行相应任务
  11. jobCounter += 1
  12. implicit val timeout = Timeout(5 seconds)
  13. val backend = backends(jobCounter % backends.size) //根据相应算法选择执行任务的节点
  14. println(s"the backend is ${backend} and the job is ${job}")
  15. val result = (backend ? job)
  16. .map(x => x.asInstanceOf[TransformationResult]) // 后台节点处理得到结果
  17. result pipeTo sender //向外部系统发送执行结果
  18.  
  19. case BackendRegistration if !backends.contains(sender()) => // 添加新的后台任务节点
  20. context watch sender() //监控相应的任务节点
  21. backends = backends :+ sender()
  22.  
  23. case Terminated(a) =>
  24. backends = backends.filterNot(_ == a) // 移除已经终止运行的节点
  25. }
  26. }

最后我们实现与集群客户端交互的逻辑:

  1. class ClientJobTransformationSendingActor extends Actor {
  2.  
  3. val initialContacts = Set(
  4. ActorPath.fromString("akka.tcp://ClusterSystem@127.0.0.1:2551/system/receptionist"))
  5. val settings = ClusterClientSettings(context.system)
  6. .withInitialContacts(initialContacts)
  7.  
  8. val c = context.system.actorOf(ClusterClient.props(settings), "demo-client")
  9.  
  10. def receive = {
  11. case TransformationResult(result) => {
  12. println(s"Client response and the result is ${result}")
  13. }
  14. case Send(counter) => {
  15. val job = TransformationJob("hello-" + counter)
  16. implicit val timeout = Timeout(5 seconds)
  17. val result = Patterns.ask(c,ClusterClient.Send("/user/frontend", job, localAffinity = true), timeout)
  18. result.onComplete {
  19. case Success(transformationResult) => {
  20. self ! transformationResult
  21. }
  22. case Failure(t) => println("An error has occured: " + t.getMessage)
  23. }
  24. }
  25. }
  26. }

下面我们开始运行这个domo:

  1. object DemoClient {
  2. def main(args : Array[String]) {
  3.  
  4. TransformationFrontendApp.main(Seq("2551").toArray) //启动集群客户端
  5. TransformationBackendApp.main(Seq("8001").toArray) //启动三个后台节点
  6. TransformationBackendApp.main(Seq("8002").toArray)
  7. TransformationBackendApp.main(Seq("8003").toArray)
  8.  
  9. val system = ActorSystem("OTHERSYSTEM")
  10. val clientJobTransformationSendingActor =
  11. system.actorOf(Props[ClientJobTransformationSendingActor],
  12. name = "clientJobTransformationSendingActor")
  13.  
  14. val counter = new AtomicInteger
  15. import system.dispatcher
  16. system.scheduler.schedule(2.seconds, 2.seconds) { //定时发送任务
  17. clientJobTransformationSendingActor ! Send(counter.incrementAndGet())
  18. }
  19. StdIn.readLine()
  20. system.terminate()
  21. }
  22. }

运行结果:

从结果可以看到,我们将任务根据算法分配给不同的后台节点进行执行,最终返回结果。

本文目的

  • 掌握集群基本概念
  • 了解学习Akka cluster的工作方式和主要角色
  • 尝试自己写一个Akka cluster的相关例子
  • 下一步进阶了解Akka cluster的背后原理

本文的demo例子已上传github:源码链接

来源于: https://godpan.me/2017/09/05/learning-akka-10.html

Akka系列(十):Akka集群之Akka Cluster的更多相关文章

  1. Redis(十)集群:Redis Cluster

    一.数据分布 1.数据分布理论 2.Redis数据分区 Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16 ...

  2. kubernetes系列(十五) - 集群调度

    1. 集群调度简介 2. 调度过程 2.1 调度过程概览 2.2 Predicate(预选) 2.3 Priorities(优选) 3. 调度的亲和性 3.1 node亲和性 3.1.1 node亲和 ...

  3. K8S从入门到放弃系列-(16)Kubernetes集群Prometheus-operator监控部署

    Prometheus Operator不同于Prometheus,Prometheus Operator是 CoreOS 开源的一套用于管理在 Kubernetes 集群上的 Prometheus 控 ...

  4. 深入剖析Redis系列: Redis集群模式搭建与原理详解

    前言 在 Redis 3.0 之前,使用 哨兵(sentinel)机制来监控各个节点之间的状态.Redis Cluster 是 Redis 的 分布式解决方案,在 3.0 版本正式推出,有效地解决了 ...

  5. Dubbo 源码分析 - 集群容错之 Cluster

    1.简介 为了避免单点故障,现在的应用至少会部署在两台服务器上.对于一些负载比较高的服务,会部署更多台服务器.这样,同一环境下的服务提供者数量会大于1.对于服务消费者来说,同一环境下出现了多个服务提供 ...

  6. redis集群之REDIS CLUSTER

    redis集群之REDIS CLUSTER 时间 2016-04-11 17:05:00  NoSQL_博客园 原文  http://www.cnblogs.com/zhanchenjin/p/537 ...

  7. Akka 集群单例Cluster Singleton

    一.简介 集群中Cluster Singleton 集群中有而只一个单例,可应用于集群全局调控,单一运算决策,中央命名服务或中央路由等应用场景 二.依赖 dependencies { compile ...

  8. redis系列--深入哨兵集群

    一.前言 在之前的系列文章中介绍了redis的入门.持久化以及复制功能,如果不了解请移步至redis系列进行阅读,当然我也是抱着学习的知识分享,如果有什么问题欢迎指正,也欢迎大家转载.而本次将介绍哨兵 ...

  9. Docker & Kubenetes 系列四:集群,扩容,升级,回滚

    本篇将会讲解应用部署到Kubenetes集群,集群副本集查看,集群自愈能力演示,集群扩容,滚动升级,以及回滚. 本篇是Docker&Kubenetes系列的第四篇,在前面的篇幅中,我们向Kub ...

随机推荐

  1. 依赖jquery的select皮肤2

    这个下拉菜单存在于body中,不会受select父级overflow的影响,同样依赖于jquery. 缺陷是如果select上的样式不是定义在class上的,不能完全获取select上的样式. 不过, ...

  2. mysql 8.0.18 手工安装记录

    mysql 8.0.18 手工安装记录 为了日常方便,特记录如下. 一.安装系统依赖包 #.系统依赖包安装 yum -y install make gcc-c++ cmake bison-devel ...

  3. sizeof运算符、字节对齐考点(面宝P50)

    记住几句话: 结构体的长度一定是最长的数据元素类型的整数倍: 某数据元素的起始地址能被该类型所占的字节数整除: 静态变量是存放在全局数据区,而sizeof计算栈中分配的大小,不包括static变量: ...

  4. 【leetcode】1234. Replace the Substring for Balanced String

    题目如下: You are given a string containing only 4 kinds of characters 'Q', 'W', 'E' and 'R'. A string i ...

  5. Java架构师面试题——JVM性能调优

    JVM内存调优 对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数. 1.Full GC 会对整个堆进行整理,包括Young.Tenured和Perm.Full GC因为需要对 ...

  6. win10 1903 更改文字大小

    标题栏 - 菜单 - 消息框 - 调色板标题11- 图标 - 工具提示 - Caption 标题 的 宽/高 - ; 14的宽高 - 菜单 的 宽/高 - ; 的宽高 -; 设置 注册表 HKEY_C ...

  7. CSS之咖啡菜单网页设计

    今天记录学习的设计网站首页的咖啡菜单,综合运用所学习的html,css背景,文本,字体,链接,表格,盒子,选择器,定位,以及css3的阴影,圆角边框,2d变换等内容. ㈠咖啡菜单整体样式 运用html ...

  8. PHP中变量声明和定义的区别

    先记录一下(不知道PHP是不是一样,但是C语言是这样的):把建立空间的声明称之为“定义”,而把不需要建立存储空间的声明称之为“声明”.声明的最终目的是为了提前使用,即在定义之前使用,如果不需要提前使用 ...

  9. 6、Lambda表达式(推荐使用)

    Lambda表达式(匿名的函数对象),是C++11增加的新特性,Qt配合信号一起使用,非常方便. pro项目文件中引入了这种特性: CONFIG += c++11 通过connect来了解Lambda ...

  10. Pod初始化容器之Init Container

    Init 容器的介绍 Pod能够具有多个容器,应用运行在容器里面,但是它也可能有一个或多个先于应用容器启动的 Init容器Init 容器与普通的容器非常像,除了如下两点:  c Init 容器总是运行 ...