Lightbend Lagom: Microservices Just Right
-
Upload
mircodotta -
Category
Software
-
view
5.102 -
download
2
Transcript of Lightbend Lagom: Microservices Just Right
Lightbend LagomMicroservices “Just Right”
Mirco Dotta
@mircodotta
Scala Days NYC - May 10, 2016
Lagom - [lah-gome]
Adequate, sufficient, just right
Agenda
● Why Lagom?● Lagom dive in
○ Development Environment○ Service API○ Persistence API
● Running in Production
Why Lagom?
● Opinionated● Developer experience matters!
○ No brittle script to run your services○ Inter-service communication just works○ Services are automatically reloaded on code change
● Takes you through to production deployment
● sbt build tool (developer experience)● Play 2.5● Akka 2.4 (clustering, streams, persistence)● Cassandra (default data store)● Jackson (JSON serialization)● Guice (DI)● Architectural Concepts: immutability, Event Sourcing/CQRS,
circuit breakers
Under the hood
Enough slides,
Demo time
Anatomy of a Lagom project
● sbt build● Scala 2.11 and JDK8● Each service definition is split into two sbt projects:
○ api○ Implementation
Service API
Service definition trait HelloService extends Service {
override def descriptor(): Descriptor = {
named("helloservice").`with`(
namedCall("/hello", sayHello _)
)
}
def sayHello(): ServiceCall[String, String]
}
// this source is placed in your api project
ServiceCall explained
● ServiceCall can be invoked when consuming a service:○ Request: type of incoming request message (e.g. String)○ Response: type of outgoing response message (e.g. String)
● JSON is the default serialization format for request/response messages● There are two kinds of request/response messages:
○ Strict○ Streamed
trait ServiceCall[Request, Response] {
def invoke(request: Request): Future[Response]
}
Strict Messages
Strict messages are fully buffered into memory
override def descriptor(): Descriptor = {
named("friendservice").`with`(
pathCall("/users/:userId/friends", addFriend _)
)
}
def addFriend(userId: String): ServiceCall[FriendId, NotUsed]
Streamed Messagesoverride def descriptor(): Descriptor = {
named("clock").`with`(
pathCall("/tick/:interval", tick())
)
}
def tick(): ServiceCall[String, Source[String, _]]
● A streamed message is of type Source (an Akka streams API)● It allows asynchronous streaming and handling of messages● Lagom will select transport protocol (currently WebSockets)
Remember the Service definition?trait HelloService extends Service {
override def descriptor(): Descriptor = {
named("helloservice").`with`(
namedCall(sayHello _)
)
}
def sayHello(): ServiceCall[String, String]
}
// this source is placed in your api project
Here is the Service implementation
class HelloServiceImpl extends HelloService {
override def sayHello(): ServiceCall[String, String] {
name => Future.successful(s"Hello, $name!")
}
}
// this source is placed in your implementation project
Inter-service communication
class MyServiceImpl @Inject()(helloService: HelloService)
(implicit ec: ExecutionContext) extends MyService {
override def sayHelloLagom(): ServiceCall[NotUsed, String] = unused => {
val response = helloService.sayHello().invoke("Lagom")
response.map(answer => s"Hello service said: $answer")
}
}
Persistence API
Principles
● Each service owns its data○ Only the service has direct access to the DB
● We advocate the use of Event Sourcing (ES) and CQRS○ ES: Capture all state’s changes as events○ CQRS: separate models for write and read
Benefits of Event Sourcing/CQRS
● Allows you to time travel● Audit log● Future business opportunities● No need for ORM● No database migration script, ever● Performance & Scalability● Testability & Debuggability
Event Sourcing: Write Side
● Create your own Command and Event classes● Subclass PersistentEntity
○ Define Command and Event handlers○ Can be accessed from anywhere in the cluster○ (corresponds to an Aggregate Root in DDD)
Event Sourcing: Example
Create the Command classes
sealed trait FriendCommand extends Jsonable
case class AddFriend(friendUserId: String) extends
PersistentEntity.ReplyType[Done] with FriendCommand
// more friend commands
Event Sourcing: Example cont’d
Create the Event classes
sealed trait FriendEvent extends Jsonable
case class FriendAdded(userId: String, friendId: String,
timestamp: Instant = Instant.now()) extends FriendEvent
// more friend events
Event Sourcing: Example cont’d
class FriendEntity extends
PersistentEntity[FriendCommand, FriendEvent, FriendState] {
def initialBehavior(snapshotState: Optional[FriendState]): Behavior =
// TODO: define command and event handlers
}
Create a subclass of PersistentEntity
Event Sourcing: Example cont’dval b: Behavior = newBehaviorBuilder(/*...*/)
b.setCommandHandler(classOf[AddFriend],
(cmd: AddFriend, ctx: CommandContext[Done]) => state.user match {
case None =>
ctx.invalidCommand(s"User ${entityId} is not created")
ctx.done()
case Some(user) =>
ctx.thenPersist(FriendAdded(user.userId, cmd.friendUserId),
(evt: FriendAdded) => ctx.reply(Done))
})
b.setEventHandler(classOf[FriendAdded],
(evt: FriendAdded) => state.addFriend(evt.friendId))
Event Sourcing: Example cont’d
No side-effects in the event handler!
Event Sourcing: Example cont’d
Create the State class
case class FriendState(user: Option[User]) extends Jsonable {
def addFriend(friendUserId: String): FriendState = user match {
case None => throw new IllegalStateException(
"friend can't be added before user is created")
case Some(user) =>
val newFriends = user.friends :+ friendUserId
FriendState(Some(user.copy(friends = newFriends)))
}
}
class FriendServiceImpl @Inject() (persistentEntities: PersistentEntityRegistry)
(implicit ec: ExecutionContext) extends FriendService {
// at service startup we must register the needed entities
persistentEntities.register(classOf[FriendEntity])
def addFriend(userId: String): ServiceCall[FriendId, NotUsed] = request => {
val ref = persistentEntities.refFor(classOf[FriendEntity], userId)
ref.ask[Done, AddFriend](AddFriend(request.friendId))
}
// ...
}
Event Sourcing: Example cont’d
Event Sourcing: Read Side
● Tightly integrated with Cassandra ● Create the query tables:
○ Subclass CassandraReadSideProcessor○ Consumes events produced by the PersistentEntity and
updates tables in Cassandra optimized for queries● Retrieving data: Cassandra Query Language
○ e.g., SELECT id, title FROM postsummary
Running in Production
● sbt-native packager is used to produce zip, MSI, RPM, Docker● Lightbend ConductR* (our container orchestration tool)● Lightbend Reactive Platform*
○ Split Brain Resolver (for Akka cluster)○ Lightbend Monitoring
*Requires a Lightbend subscription (ConductR is free to use during development)
Current[Lagom]
● Current version is 1.0.0-M2○ 1.0 soon
● Java API, but no Scala API yet○ We are working on the Scala API○ But using Scala with the Java API works well! https:
//github.com/dotta/activator-lagom-scala-chirper
Future[Lagom]
● Maven support● Message broker integration● Scala API● Support for other cluster orchestration tools● Support for writing integration tests● Swagger integration
○ Which also removes binary coupling!
Next: Seq[Step]● Try Lagom yourself
○ https://lightbend.com/lagom● Using Scala with Lagom
○ https://github.com/dotta/activator-lagom-scala-chirper● Lagom on Github
○ https://github.com/lagom/lagom● Read Jonas Bonér's free book Reactive Services Architecture
○ https://lightbend.com/reactive-microservices-architecture ● Great presentation by Greg Young on why you should use ES
○ https://www.youtube.com/watch?v=JHGkaShoyNs
Thank you for listening!@mircodotta
@lagom
override def descriptor(): Descriptor = {
named("friendservice").`with`(
namedCall("/users", createUser _),
pathCall("/users/:id", getUser _),
restCall(Method.POST, "/users/:userId/friends", addFriend _)
)
}
def createUser(): ServiceCall[User, NotUsed]
def getUser(id: String): ServiceCall[NotUsed, User]
def addFriend(userId: String): ServiceCall[FriendId, NotUsed]
Different kinds of service call descriptors