skaka的博客

every flight begins with a fall

微服务框架Finagle介绍 Part1: Future, Service, Filter

| Comments

微服务架构可能是时下最热的一种架构模式了. 这篇系列里, 我想介绍一些常用的微服务框架. 通过学习这些框架, 我们将会了解实现微服务的过程中会遇到哪些问题, 以及这些微服务框架是如何帮助我们解决这些问题的. 所以这是一篇关于微服务实践的系列, 我不会讨论太多概念性的东西. 系列末尾我会给出一些微服务架构相关的链接, 感兴趣的可以参考.

微服务不同于单一架构应用, 是典型的分布式场景, 各服务之间通过IPC进行通信. 实现微服务的过程中, 我们需要解决以下问题:
1. 服务注册和服务发现.
2. 根据应用选择合适的通信协议和数据协议. 例如可以选用thrift, protocol buffer或REST.
3. 服务负载均衡. 一个服务一般会部署多个实例. 如果使压力均匀分布是需要考虑的问题.
4. 服务路由与限流.
5. 容错处理. 相对于单机应用, 分布式环境下错误发生的概率会大大提高, 服务宕机, 网络不可用的情况时常发生.
6. 服务监控. 各服务实例的性能指标, 例如请求响应时间, 请求并发数量, 以及服务实例的部署数量等.
7. 事务一致性. 一般来说这个问题需要我们结合业务自己处理, 框架不会给我们太多帮助.

好的微服务框架应该能帮助我们解决上面的全部或者大部分问题. 这里我选择JVM上比较热门的三个微服务框架: Finagle, Spring Cloud(NetflixOSS), Dubbox. 我会从实例入手, 介绍这些框架的使用方式, 特点和适用场景.

首先来看Finagle. Finagle是Twitter在2011年开源的一款RPC框架, 在国外使用较多, 例如Pinterest, Nest, Tumblr, 感兴趣的可以Google. Finagle有着较为丰富的生态圈, 例如可以使用Finch很方便的实现REST, 使用Finagle OAuth2实现OAuth认证, 使用zipkin实现服务监控. Finagle使用Scala开发, 官方宣称同时支持Scala和Java语言.

学习Finagle的使用之前, 首先要了解Finagle中的三个核心概念: Future, Service, Filter.

1. Future

Finagle使用的Future是com.twitter.util.Future. 由于Future非常实用, 从Scala2.10开始被加入到官方库scala.concureent.Future. Java8中也引入了一个类似的接口java.util.concurrent.CompletableFuture. Future是对异步操作的抽象, 你可以将Future理解为一个容器, 这个容器包含一个异步操作. 一个Future容器可能处于三个状态中的一种: 异步操作还没有完成, 操作已经完成了并包含了成功结果, 操作失败并包含了异常结果. Future一种很常用的用法是可以注册成功或失败的回调函数, 例如下面的Java代码:

1
2
3
4
5
6
7
8
9
responseFuture.onSuccess(func(response -> {
    System.out.println(String.format("response status: %s, response string: %s",
            response.status().toString(), response.contentString()));
    return BoxedUnit.UNIT;
}));
responseFuture.onFailure(func(e -> {
    System.out.println("error: " + e.toString());
    return BoxedUnit.UNIT;
}));

我在responseFuture上注册了一个成功的回调函数和失败的回调函数, 当Future对应的操作完成时, 会简单的打印出结果或异常信息. Future另外一个十分强大的用法是组合.例如下面的Java代码:

1
2
3
4
5
Future<User> authenticatedUser = User.authenticate(email, password)
 
Future<Seq<Tweet>> lookupTweets = authenticatedUser.flatMap(user -> Tweet.findAllByUser(user))

//#1

这段代码首先根据email和password获取user对象, 然后获取user对应的所有微博. 我解释下这段代码的执行逻辑. 首先调用User.authenticate(email, password)方法进行用户认证, 返回的对象是Future, 代表这是一个异步操作. 注意我们拿到的是Future, 这个时候我们还没有真正的拿到user对象. 接下来flatMap方法就派上用场了. 在上面代码中, flatMap函数签名应该是这样的:

1
2
3
4
5
//Java8中并没有这个函数, 这里只是用来解释概念.
//Java8中CompletableFuture的thenCompose方法类似于flatMap
Future<Seq<Tweet>> flatMap(Function<User, Future<Seq<Tweet>>) {
  //...
}

简单来说, flatMap的作用是将Future<A>转换成Future<B>, 在这个例子里, 是将Future<User>转换成Future<Seq<Tweet>>. 通过flatMap这种方式, 我们的代码写起来很像是同步执行的, 但是实际上Future中的操作是由一个叫做Scheduler的组件去执行的, 你可以将Scheduler理解为一个ExecutorService, 即我们的代码是由其他线程异步执行的. 上面的代码中, 当代码执行到#1位置的时候, 其实认证用户和获取微博这两个操作可能并没有真正被执行.

Future与flatMap的概念都来源于函数式编程. 在Haskell中, flatMap叫做绑定(bind), 而Future可以近似看作Monad(单子). 对函数式编程中的Monad感兴趣的朋友可以参考我之前的文章.

Future还有其他一些很有用的方法, 例如从异常中恢复的rescue方法, 连接多个Future的join方法等, 这里就不展开了. Future在Finagle中无处不在, Finagle的设计哲理之一就是能异步的尽量异步, 大部分操作都不会阻塞. 例如下面我们要说的Service和Filter, 返回的结果都是Future. 如果你之前主要使用Spring或者Servlet这种技术, 可能刚学习Finagle的时候觉得有些难以理解. 这很正常, 在后面的文章我会详细介绍如何使用Future编程, 你会发现其实这种异步编程习惯与之前相比没有太大的不同.只是ThreadLocal在这种环境下失效了, 不过好在我们有替代品 :)

2. Service

Service是Finagle中的核心概念. Service可以被理解为接收一个Request参数, 返回一个Future对象的函数. 如果定义为Java的抽象类, 原型如下:

1
2
3
4
5
6
//Service在Finagle中是用Scala代码定义的, 这里只是用来解释概念.
public abstract class Service<Request, Response> {

    public abstract Future<Response> apply(Request r);

}

如果用Spring MVC类比, Finagle的Service就类似于Controller的方法, 可以用来处理客户端的请求. 例如要在Finagle中实现一个Echo服务器, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Server extends Service<Request, Response> {

    @Override
    public Future<Response> apply(Request request) {
        Response response = Response.apply(Version.Http11$.MODULE$, Status.Ok());
        response.setContentString(request.getContentString());
        return Future.value(response);
    }

    public static void main(String[] args) throws Exception {
        Server service = new Server();

        ListeningServer server = Http.server().
                withLabel("echo-server").
                serve(new InetSocketAddress(8081), service);

        Await.result(server);
    }
}

注意Service的返回值是Future, 代表操作可以是异步完成的.

3. Filter

Finagle Filter类似于Servlet Filter, 可以对Service的请求和响应进行过滤. 不过Finagle Filter使用类型参数明确定义了 输入输出的参数类型, Finagle Filter如果定义为Java的抽象类, 原型如下:

1
2
3
4
5
6
//Filter在Finagle中是用Scala代码定义的, 这里只是用来解释概念.
public abstract class Filter<ReqIn, RepOut, ReqOut, RepIn> {

     public abstract Future<RepOut> apply(ReqIn request, Service<ReqOut, RepIn> service);

}

对于ReqIn, RepOut, ReqOut, RepIn这四个类型参数的定义, 可以参考下图.

ReqIn和ReqOut分别是Filter的入参和出参, 而RepIn和RepOut则是Service的入参和出参. 我们来看看Filter在代码中的实际用法:

1
2
3
4
5
6
7
8
9
10
val baseService = new Service[HttpRequest, HttpResponse] {
  def apply(request: HttpRequest) =
    Future(new DefaultHttpResponse(HTTP_1_1, OK))
}
 
val authorize = new RequireAuthorization()
val handleExceptions = new HandleExceptions(...)
 
val decoratedService: Service[HttpRequest, HttpResponse] =
  handleExceptions andThen authorize andThen baseService

我们定义了一个Service对象baseService, 两个Filter对象authorize和handleExceptions. 通过filter的andThen方法, 我们能够很简单的将Filter和Service组装到一起, 这有点类似于在web.xml中定义了一个Servlet, 以及两个Filter来拦截针对Servlet的请求. 不过毫无疑问Finagle这种使用方式更加直观, 并且不容易出错.

现在我们已经了解了Finagle的基本概念, 下一篇我将结合实例介绍如何使用Finagle进行开发.

Comments