akka-typed(9) - 业务分片、整合,谈谈lagom, 需要吗?
在讨论lagom之前,先从遇到的需求开始介绍:现代企业的it系统变得越来越多元化、复杂化了。线上、线下各种系统必须用某种方式集成在一起。从各种it系统的基本共性分析:最明显的特征应该是后台数据库的角色了,起码,大家都需要使用数据。另外,每个系统都可能具备大量实时在线用户、海量数据特性,代表着对数据处理能力有极大的要求,预示系统只有通过分布式处理方式才能有效运行。
一个月前开始设计一个企业的it系统,在讨论数据中台时就遇到这样的需求。这个所谓的数据中台的主要作用是为整体系统提供一套统一的数据使用api,前后连接包括web,mobile,desktop的前端系统以及由多种传统及分布式数据库系统,形成一个统一的数据使用接口。实际上,数据库连接不只是简单的读写操作,还需要包括所有实时的数据处理:根据业务要求对数据进行相应的处理然后使用。那么这是一个怎样的系统呢?首先,它必须是分布式的:为了对付大量的前端用户同时调用同一个api,把这个api的功能同时分派到多个服务器上运行是个有效的解决方法。这是个akka-cluster-sharding模式。数据中台api是向所有内部系统以及一些特定的外部第三方系统开放的,用http标准协议支持各系统与数据后台的连接也是合理的。这个akka-http, akka-grpc可以胜任。然后各系统之间的集成可以通过一个流运算工具如kafka实现各聚合根之间的交互连接。
似乎所有需要的工具都齐备了,其中akka占了大部分功能。但有些问题是:基于akka技术栈来编程或多或少有些门槛要求。最起码需要一定程度的akka开发经验。更不用提组织一个开发团队了。如果市面上有个什么能提供相应能力的开发工具,可以轻松快速上手的,那么项目开发就可以立即启动了。
现在来谈谈lagom:lagom是一套scala栈的微服务软件开发工具。从官方文档介绍了解到lagom主要提供了一套服务接口定义及服务功能开发框架。值得一提的是服务功能可以是集群分片模式的。走了一遍lagom的启动示范代码,感觉这是一套集开发、测试、部署为一体的框架(framework)。在这个框架里按照规定开发几个简单的服务api非常顺利,很方便。这让我对使用lagom产生了兴趣,想继续调研一下利用lagoom来开发上面所提及数据中台的可行性。lagom服务接入部分是通过play实现的。play我不太熟悉,想深入了解一下用akka-http替代的可行性,不过看来不太容易。最让我感到失望的是lagom的服务分片(service-sharding)直接就是akka-cluster那一套:cluster、event-sourcing、CQRS什么的都需要自己从头到尾重新编写。用嵌入的kafka进行服务整合与单独用kafka也不会增加太多麻烦。倒是lagom提供的这个集开发、测试、部署为一体的框架在团队开发管理中应该能发挥良好的作用。
在我看来:服务接入方面由于涉及身份验证、使用权限、二进制文件类型数据交换等使用akka-http,akka-grpc会更有控制力。服务功能实现直接就用akka-cluster-sharding,把计算任务分布到各节点上,这个我们前面已经介绍过了。
所以,最后还是决定直接用akka-typed来实现这个数据中台。用了一个多月时间做研发,到现在看来效果不错,能够符合项目要求。下面是一些用akka-typed实现业务集成的过程介绍。首先,系统特点是功能分片:系统按业务条块分成多个片shardregion,每个片里的entity负责处理一项业务的多个功能。多个用户调用一项业务功能代表多个entity分布在不同的集群节点上并行运算。下面是一个业务群的代码示范:
object Shards extends LogSupport {
def apply(mgoHosts: List[String],trace: Boolean, keepAlive: FiniteDuration, pocurl: String)(
implicit authBase: AuthBase): Behavior[Nothing] = {
Behaviors.setup[Nothing] { ctx =>
val sharding = ClusterSharding(ctx.system)
log.stepOn = true
log.step(s"starting cluster-monitor ...")(MachineId("",""))
ctx.spawn(MonitorActor(),"abs-cluster-monitor")
log.step(s"initializing sharding for ${Authenticator.EntityKey} ...")(MachineId("",""))
val authEntityType = Entity(Authenticator.EntityKey) { entityContext =>
Authenticator(entityContext.shard,mgoHosts,trace,keepAlive)
}.withStopMessage(Authenticator.StopAuthenticator)
sharding.init(authEntityType)
log.step(s"initializing sharding for ${CrmWorker.EntityKey} ...")(MachineId("",""))
val crmEntityType = Entity(CrmWorker.EntityKey) { entityContext =>
CrmWorker(entityContext.shard,mgoHosts,entityContext.entityId,trace,keepAlive)
}.withStopMessage(CrmWorker.StopWorker)
sharding.init(crmEntityType)
log.step(s"initializing sharding for ${GateKeeper.EntityKey} ...")(MachineId("",""))
val gateEntityType = Entity(GateKeeper.EntityKey) { entityContext =>
GateKeeper(entityContext.shard,mgoHosts,entityContext.entityId,trace,keepAlive)
}.withStopMessage(GateKeeper.StopGateKeeper)
sharding.init(gateEntityType)
log.step(s"initializing sharding for ${PluWorker.EntityKey} ...")(MachineId("",""))
val pluEntityType = Entity(PluWorker.EntityKey) { entityContext =>
PluWorker(entityContext.shard,mgoHosts,entityContext.entityId,trace,keepAlive)
}.withStopMessage(PluWorker.StopWorker)
sharding.init(pluEntityType)
log.step(s"initializing sharding for ${PocWorker.EntityKey} ...")(MachineId("",""))
val pocEntityType = Entity(PocWorker.EntityKey) { entityContext =>
PocWorker(entityContext.shard,mgoHosts,entityContext.entityId,trace,keepAlive,pocurl)
}.withStopMessage(PocWorker.StopWorker)
sharding.init(pocEntityType)
Behaviors.empty
}
}
}
可以看到,不同类型的片以不同的EntityKey来代表。前端接入是基于akka-http的,如下:
object CrmRoute extends LogSupport {
def route(entityRef: EntityRef[CrmWorker.Command])(
implicit ec: ExecutionContext, jsStreaming: EntityStreamingSupport, timeout: Timeout): akka.http.scaladsl.server.Route = {
concat(
pathPrefix("ismember") {
parameter(Symbol("faceid")) { fid =>
val futResp = entityRef.ask[CrmWorker.Response](CrmWorker.IsMemberFace(fid, _))
.map {
case CrmWorker.ValidMember(memberId) => memberId
case CrmWorker.InvalidMember(msg) => throw new Exception(msg)
}
onSuccess(futResp)(complete(_))
}
},
pathPrefix("getmember") {
parameter(Symbol("memberid")) { mid =>
val futResp = entityRef.ask[CrmWorker.Response](CrmWorker.GetMemberInfo(mid, _))
.map {
case CrmWorker.MemberInfo(json) => HttpEntity(MediaTypes.`application/json`,json)
case CrmWorker.InvalidMemberInfo(msg) => throw new Exception(msg)
}
onSuccess(futResp)(complete(_))
}
}
)
}
}
各项业务功能调用通过entityRef.ask发送给了某个用户指定节点上的entity。akka的actor是线程的再细分,即一个actor可能与其它成千上万个actor共享一条线程。所以绝对不容许任何blocking。我是用下面示范的模式来实现non-blocking的:
def apply(shard: ActorRef[ClusterSharding.ShardCommand],mgoHosts: List[String], entityId: String, trace: Boolean, keepAlive: FiniteDuration): Behavior[Command] = {
val (shopId,posId) = entityId.split(':').toList match {
case sid::pid::Nil => (sid,pid) }
implicit val loc = Messages.MachineId(shopId,posId)
log.stepOn = trace
// Behaviors.supervise(
Behaviors.setup[Command] { ctx =>
implicit val ec = ctx.executionContext
ctx.setReceiveTimeout(keepAlive, Idle)
Behaviors.withTimers[Command] { timer =>
Behaviors.receiveMessage[Command] {
case IsMemberFace(fid, replyTo) =>
log.step(s"CrmWorker: IsMemberFace($fid)")
implicit val client = mongoClient(mgoHosts)
maybeMgoClient = Some(client)
ctx.pipeToSelf(isMemberFace(fid)) {
case Success(mid) => {
if (mid._1.isEmpty) {
replyTo ! InvalidMember(mid._2)
Done(loc.shopid, loc.posid, s"IsMemberFace with Error ${mid._2}")
} else {
replyTo ! ValidMember(mid._1)
Done(loc.shopid, loc.posid, s"IsMemberFace.")
}
}
case Failure(err) =>
log.error(s"CrmWorker: IsMemberFace Error: ${err.getMessage}")
replyTo ! InvalidMember(err.getMessage)
Done(loc.shopid, loc.posid, s"IsMemberFace with error: ${err.getMessage}")
}
Behaviors.same
case GetMemberInfo(mid, replyTo) =>
log.step(s"CrmWorker: GetMemberInfo($mid)")
implicit val client = mongoClient(mgoHosts)
maybeMgoClient = Some(client)
ctx.pipeToSelf(getMemberInfo(mid)) {
case Success(json) => {
replyTo ! MemberInfo(json)
Done(loc.shopid, loc.posid, s"GetMemberInfo with json ${json}")
}
case Failure(err) =>
log.error(s"CrmWorker: GetMemberInfo Error: ${err.getMessage}")
replyTo ! InvalidMemberInfo(err.getMessage)
Done(loc.shopid, loc.posid, s"GetMemberInfo with error: ${err.getMessage}")
}
Behaviors.same
case Idle =>
// after receive timeout
shard ! ClusterSharding.Passivate(ctx.self)
Behaviors.same
case StopWorker =>
Behaviors.stopped(
() => log.step(s"CrmWorker: {$shopId,$posId} passivated to stop.")(MachineId(shopId, posId))
)
case Done(shopid, termid, work) =>
if (maybeMgoClient.isDefined)
maybeMgoClient.get.close()
log.step(s"CrmWorker: {$shopid,$termid} finished $work")(MachineId(shopid,termid))
Behaviors.same
case _ => Behaviors.same
}.receiveSignal {
case (_,PostStop) =>
log.step(s"CrmWorker: {$shopId,$posId} stopped.")(MachineId(shopId, posId))
Behaviors.same
}
}
}
// ).onFailure(SupervisorStrategy.restart)
}
主要是使用ctx.pipeToSelf(work)把一个Future转换成内部消息。这里的work的实现最终必须返回Future类型,如下面的示范:
object CrmServices extends JsonConverter with LogSupport {
import MgoHelpers._
def validMember(docs: Seq[Document], faceid: String): Future[(String,String)] = {
val memberId: (String, String) = docs match {
case Nil => ("", s"faceid[$faceid]不存在!")
case docs =>
val member = MemberInfo.fromDocument(docs.head)
if (member.expireDt.compareTo(mgoDateTimeNow) < )
("", s"会员:${member.memberId}-${member.memberName}会籍已过期!")
else
(member.memberId, "")
}
FastFuture.successful(memberId)
}
def isMemberFace(faceid: String)(
implicit mgoClient: MongoClient, ec: ExecutionContext): Future[(String,String)] = {
implicit val db = mgoClient.getDatabase(CrmModels.SCHEMA.DBNAME)
val col = db.getCollection(CrmModels.SCHEMA.MEMBERINFO)
val memberInfo: Future[Seq[Document]] = col.find(equal(SCHEMA.FACEID,faceid)).toFuture()
for {
mi <- memberInfo
(id,msg) <- validMember(mi,faceid)
} yield (id,msg)
}
def getMemberInfo(memberid: String)(
implicit mgoClient: MongoClient, ec: ExecutionContext): Future[String] = {
implicit val db = mgoClient.getDatabase(CrmModels.SCHEMA.DBNAME)
val col = db.getCollection(CrmModels.SCHEMA.MEMBERINFO)
val memberInfo: Future[Seq[Document]] = col.find(equal(SCHEMA.MEMBERID,memberid)).toFuture()
for {
docs <- memberInfo
jstr <- FastFuture.successful(if(docs.isEmpty) "" else toJson(MemberInfo.fromDocument(docs.head)))
} yield jstr
}
}
另外,由于每个用户第一次调用一项业务功能时akka-cluster-shardregion都会自动在某个节点上构建一个新的entity,如果上万个用户使用过某个功能,那么就会有万个entity及其所占用的资源如mongodb客户端等停留在内存里。所以在完成一项功能运算后应关闭entity,释放占用的资源。这个是通过shard ! ClusterSharding.passivate(ctx.self)实现的。
akka-typed(9) - 业务分片、整合,谈谈lagom, 需要吗?的更多相关文章
- Akka Typed 官方文档之随手记
️ 引言 近两年,一直在折腾用FP与OO共存的编程语言Scala,采取以函数式编程为主的方式,结合TDD和BDD的手段,采用Domain Driven Design的方法学,去构造DDDD应用(Dom ...
- Akka Typed系列:协议&行为
引言 2019年11月6号LightBend公司发布了AKKA 2.6版本,带来了类型安全的actor,新的Akka Cluster底层通信设施——Artery,带来了更好的稳定性,使用Jackson ...
- Akka源码分析-Akka Typed
对不起,akka typed 我是不准备进行源码分析的,首先这个库的API还没有release,所以会may change,也就意味着其概念和设计包括API都会修改,基本就没有再深入分析源码的意义了. ...
- RPA应用场景-公积金贷款业务数据整合和报送
场景概述 公积金贷款业务数据整合和报送 所涉系统名称 个贷系统.公积金管理系统 人工操作(时间/次) 0.5小时 所涉人工数量1000操作频率 每日 场景流程 1.机器人整理个人贷款信息.个人贷款账户 ...
- Lagom学习 六 Akka Stream
lagom中的stream 流数据处理是基于akka stream的,异步的处理流数据的.如下看代码: 流式service好处是: A: 并行: hellos.mapAsync(8, name -& ...
- 【整合篇】Activiti业务与流程的整合
对于不管是Activtit还是jbpm来说,业务与流程的整合均类似.启动流程是绑定业务.流程与业务的整合放到动态代理中 [java] view plain copy print" style ...
- akka 集群分片
akka 集群 Sharding分片 分片上下级结构 集群(多台节点机) —> 每台节点机(1个片区) —> 每个片区(多个分片) —> 每个分片(多个实体) 实体: 分片管理的 A ...
- Lagom 官方文档之随手记
引言 Lagom是出品Akka的Lightbend公司推出的一个微服务框架,目前最新版本为1.6.2.Lagom一词出自瑞典语,意为"适量". https://www.lagomf ...
- 简述在akka中发送消息的过程
在flink的数据传输过程中,有两类数据,一类数据是控制流数据,比如提交作业,比如连接jm,另一类数据是业务数据.flink对此采用了不同的传输机制,控制流数据的传输采用akka进行,业务类数据传输在 ...
随机推荐
- Swift开发笔记
Swift开发笔记(一) 刚开始接触XCode时,整个操作逻辑与Android Studio.Visual Studio等是完全不同的,因此本文围绕IOS中控件的设置.事件的注册来简单的了解IOS开发 ...
- 年薪30W+高薪测试技术要掌握哪些?
职业技能一 1. 软件测试: 1) 熟练灵活地运用等价类.边界值.判定表法.因果图法等各种方法设计测试用例,包括单元测试.集成测试.系统测试用例设计. 2) 牢固掌握了软件测试计划.测试日报.测试报告 ...
- vue : 检测用户上传的图片的宽高
需求: 用户可上传3-6张图片(第 1 2 3 张必须传),上传的图片必须是540 * 330 像素. 第一步,获取上传的图片的宽高. 初始化一个对象数组,宽高均设为0. 如果用户上传的图片没有上限, ...
- python学完可以做什么?Python就业方向最全面的解析
乔布斯说过:“每一个人都应该学习如何编程,因为编程会教会你如何思考.”下一个时代是人机交互的时代,学习编程不是要让你成为程序员,而让你理解这个时代. 点击免费领取:全网最全python学习导图+14张 ...
- Redis的字符串底层是啥?为了速度和安全做了啥?
面试场景 面试官:Redis有哪些数据类型? 我:String,List,set,zset,hash 面试官:没了? 我:哦哦哦,还有HyperLogLog,bitMap,GeoHash,BloomF ...
- p44_IP数据包格式
一.IP数据报格式 二.IP分片 数据链路层每帧可封装数据有上限,IP数据超过的要分片. 标识:同一数据报的分片使用同一标识 标志: 片偏移(13bit):用于还原数据报顺序,指出某片在原分组1中的相 ...
- 通过PHP工具箱-站点域名管理(创建本地虚拟主机)
工具:php程序员工具箱(网上很多请自己搜索下载) 1.点击其它选项菜单 -> 选择站点域名管理.如下图 2.进入站点域名管理.如下图(初始的时候,站点为空) 3.设置站点管理.如下图 网站域名 ...
- 计算滚动条的宽度--js
原理 创建两个div嵌套在一起 外层的div设置固定宽度和overflow:scroll 滚动条的宽度=外层div的offsetWidth-内层div的offsetWidth 实现代码 /** * 获 ...
- tomcat 认证爆破之custom iterator使用
众所周知,BurpSuite是渗透测试最基本的工具,也可是神器,该神器有非常之多的模块:反正,每次翻看大佬们使用其的骚操作感到惊叹,这次我用其爆破模块的迭代器模式来练练手[不喜勿喷] 借助vulhub ...
- flask中url_for使用endpoint和视图函数名
在flask中,使用url_for 进行路由反转时,需要传递一个endpoint的值,用法如下: @app.route('/', endpoint='my_index') def index(): r ...