微服务框架 Colossus

一直认为 Akka 是 JVM 上实现微服务框架的最理想基础设施,现在 Colossus 终于来了!

Tumblr 开源了其用 Scala 实现的微服务框架 Colossus。Colossus 是轻量级的 Scala 微服务 I/O 框架,基于 Akka Actor 实现,项目地址请戳 http://github.com/tumblr/colossus

Tumblr 面临的最大问题之一是如何架设及扩展不断持续增长的服务平台。这是病得治,微服务架构是药。它将小而专的应用封装成一个特性或组件,抛弃之前那种一个独立的大应用包含整站逻辑的做法。

微服务框架提供清晰的职责分离,有助于促进建造良好的基础设施,更容易排查缺陷和性能瓶颈。微服务有如此多的优点,但同样也面临挑战,如微服务要易于构建、维护、部署和监控,最重要的是他们需要极高的性能和容错性。单个服务每秒必须响应数万或数十万请求,并且有严格的延迟时间和正常运行时间要求。

Colossus 是 Tumblr 用来解决如上问题的新框架。他提供一个轻量级、简单的模型,来创建高性能的微服务。他用 scala 实现,基于 NIO 和 Akka Actor。该框架对 Tumblr 内部的服务搭建产生了极大的影响。

微服务在 Tumblr 不是新鲜事物,过去旧的框架很难写出服务高性能、高稳定性、高可用的服务。构建一个服务,只有少数几个具有领域知识的工程师才能高效完成的。Colossus 的出现改变了这一局面,使得更容易开发快速的容错服务,大大降低了准入门槛。

Colossus 主要有两个目标:

性能

目前,最重要的目标就是 Colossus 的程序应该至少与直接使用NIO(不使用任何框架)写的程序一样快。Colossus 是设计用来封装那些直接使用 NIO 服务的 I/O 层,目前的已有框架都有性能问题。因此,我们要重新打造一个无损性能的框架。

微服务架构用来并发处理来自各种客户端大量无状态小请求。Reactor 模型是一种实现方案,他使用单线程的事件循环去处理多元的客户端 TCP 连接。Colossus 提供一个干净的此种模型的实现,保证尽可能少的开销。在很多情况下,并发的代码不容易写,实际上使用Futures 和 Akka actors 是简单高效的,他是真正的并行。

这种混合 Actor/Reactor 模型保证了所需的性能。我们基准测试达到了数百万QPS,一些生产系统已经处理数千亿的请求,其中 99.99% 延迟都在5毫秒以内。

简单

Colossus 的另一个目标是小而聚焦,低的准入门槛。简单包含两个方面:框架自身简单和框架用起来简单。

框架自身简单指的是 Colossus 只聚焦于微服务。为此 Colossus 的核心是一个广义的 NIO 包装,我们的大部分工作都在微服务用例,来保证代码的小而简单直接。

框架用起来简单指的是API层面,这主要用到 Scala 语言的优势。Scala 的最大优点是极具表现力的代码和设计简单的DSL。此外,因为scala重视类型安全性和函数式编程,我们确保 Colossus 尽量集成这些优点。这导致程序猿写应用更简单,更聚焦于业务逻辑而不是代码自身。

这些原则使得 Colossus 变成 Tumblr 基础设施的基本组成部分,并使 Tumblr 走向一个更加面向服务的体系结构。 Colossus 已经大大改善了 Tumblr 内部构造服务的方式,在生产系统系统中取得巨大的成功。

libraryDependencies += "com.tumblr" % "colossus_2.11" % "0.5.1-M1"

a little http server

import colossus._
import service._
import protocols.http._
import UrlParsing._
import HttpMethod._
object Main extends App {
implicit val io_system = IOSystem()
Service.become[Http]("http-echo", 9000){
case request @ Get on Root => request.ok("Hello world!")
case request @ Get on Root / "echo" / str => request.ok(str)
}
}

KeyValExample

class KeyValDB extends Actor {
import KeyValDB._
val db = collection.mutable.Map[ByteString, ByteString]()
def receive = {
case Get(key, promise) => promise.success(db.get(key))
case Set(key, value, promise) => {
db(key) = value
promise.success(())
}
}
}
object KeyValDB {
case class Get(key: ByteString, promise: Promise[Option[ByteString]] = Promise())
case class Set(key: ByteString, value: ByteString, promise: Promise[Unit] = Promise())
}
object KeyValExample {
def start(port: Int)(implicit io: IOSystem): ServerRef = {
import io.actorSystem.dispatcher
val db = io.actorSystem.actorOf(Props[KeyValDB])
Service.become[Redis]("key-value-example", port){
case Command("GET", args) => {
val dbCmd = KeyValDB.Get(args(0))
db ! dbCmd
dbCmd.promise.future.map{
case Some(value) => BulkReply(value)
case None => NilReply
}
}
case Command("SET", args) => {
val dbCmd = KeyValDB.Set(args(0), args(1))
db ! dbCmd
dbCmd.promise.future.map{_ =>
StatusReply("OK")
}
}
}
}
}

HttpExample

object HttpExample {
/**
* Here we're demonstrating a common way of breaking out the business logic
* from the server setup, which makes functional testing easy
*/
class HttpRoutes(redis: LocalClient[Command, Reply]) {
def invalidReply(reply: Reply) = s"Invalid reply from redis $reply"
val handler: PartialFunction[HttpRequest, Response[HttpResponse]] = {
case req @ Get on Root => req.ok("Hello World!")
case req @ Get on Root / "get" / key => redis.send(Commands.Get(ByteString(key))).map{
case BulkReply(data) => req.ok(data.utf8String)
case NilReply => req.notFound("(nil)")
case other => req.error(invalidReply(other))
}
case req @ Get on Root / "set" / key / value => redis.send(Commands.Set(ByteString(key), ByteString(value))).map{
case StatusReply(msg) => req.ok(msg)
case other => req.error(invalidReply(other))
}
}
}
def start(port: Int, redisAddress: InetSocketAddress)(implicit system: IOSystem): ServerRef = {
Service.serve[Http]("http-example", port){context =>
val redis = context.clientFor[Redis](redisAddress.getHostName, redisAddress.getPort)
//because our routes object has no internal state, we can share it among
//connections. If the class did have some per-connection internal state,
//we'd just create one per connection
val routes = new HttpRoutes(redis)
context.handle{connection =>
connection.become(routes.handler)
}
}
}
}

tumblr colossus

参考文献:

  1. Colossus: A New Service Framework from Tumblr
  2. Tumblr推出开源微服务框架Colossus
  3. Github: tumblr colossus
  4. Colossus Page
  5. Colossus Documentation

评论