谈谈服务化体系中的异步(上)

一个懂Akka、RxJava,看得懂《七周七并发》的人,和普通程序员完全是两个世界的人。
那作为一个羞涩的普通程序员,怎么在自己的服务化体系里,满足自己的异步化需求呢 ?我的思路是这样的:

1. 先认清自己的需求,不要一开始就站到某个技术上,再倒叙自己的需求。

2. 然后从浅入深,分析可以用什么样的技术满足该层次的需求,依然避免一下就冲到某个技术上,比如Akka入门到放弃。

3. 最后找出一条相对平稳的技术路线,能够贯穿各个层次的需求,而且最好不那么考验使用者的智商。

 

1. 需求篇

1.1 通过并行的远程调用,缩短总的响应延时。

1.1.1 远程调用分类

服务处理的过程中,总会包含如下几类的远程调用:

服务层:RESTful,RPC框架
数据层:JDBC,Memcached,Redis,ZooKeeper...
消息层:Kafka,RabbitMQ...

有些客户端已经支持异步,比如大部分的RPC框架、RESTful Client、SpyMemcached、ZooKeeper、Kafka 等。 在异步接口之中,又分返回Future 与 用户自定义的CallBack类两种风格。

但有些客户端还是同步的,比如JDBC,Jedis,在设计方案时需要把它们也考虑进去。

1.1.2 并行执行的机会有两种

一是服务提供者没有提供批量接口,比如商品查询,只能以不同的ID分次调用(题外话,始终服务方提供批量接口会更好些)。

一是两个调用之间没有依赖关系,包括参数依赖(一个调用的参数依赖另一个调用的返回结果),时间依赖(一个调用必需在另一个调用成功后执行)。

1.1.3 并行调用的复杂度分两个层次

一是简单并行,把所有可并行的服务一下子都撒出去,然后等待所有的异步调用返回,简单的Future.get()就够用。

还有一种场景是,先调一个异步接口,然后做一些同步的事情,再回头Future.get()拿之前异步调用的结果,这也达到了节省总响应时间的效果。

二是调用编排,比如一开始并行A、B、C、D服务,一旦A与B返回则根据结果调用E,一旦C返回则调用F,最后组装D、E、F的结果返回给客户端。

如果还是简单的并行,没办法做到最高效的调度,必须有一种机制,支持这种多条异步调用链分头并进的场景。此时就需要或Akka,或RxJava,或JDK8的CompletableFuture与Guava的ListenenableFuture,基于Actor,Rx or Callback机制来编排了。再后来,发现其实依然使用Future.get()多起几条线程也一样能做,不要先被某个技术迷住。

 

1.2 希望能用少量的固定线程,处理海量的并发请求。

这个概念并不陌生,NIO就是这个思路。

但是即使你用了Netty的NIO 或 Servlet3.0的异步Servlet 或,也只是解决了传输层面用少量传输线程处理海量并发的传输,并在入口层的编程模式上提供了异步化的可能性。

如果你的服务线程里存在阻塞的远程调用,那线程还是会等待在远程调用上而无法处理海量请求,即使异步化如Future.get(),也依然是等待。

所以,你必须有办法在阻塞等待时把线程给交回去,比如服务线程里采用全异步化的Callback模式,或引入Akka的Actor模式,或基于Quasar引入纤程协程的概念。

 

1.3 小结

在我看来 , 客户端简单并行->客户端并行编排->服务端少量线程处理海量请求。对于大部分普通项目,是由浅入深三个层次的需求。
 

2.第一层,简单并行实现篇

2.1 最简单写法

最简单就是并发的调用一堆返回Future的异步接口,再一个个Get回来,最慢的那个会阻塞住其他:

Future<Product> productFuture = productService.query(id);
Future<Long> stockFuture = stockService.query(id);
.......
Product product = productFuture.get();
Long stock = stockFuture.get();

HttpClient有一个HttpAsyncClient 的子项目提供Http的异步访问:

HttpGet request1 = new HttpGet("http://www.apache.org/");
Future<HttpResponse> future = httpclient.execute(request1, null);
HttpResponse response1 = future.get();

Spring的RestTemplate同样有AsyncRestTemplate:

Future<ResponseEntity<String>> futureEntity = template.getForEntity("http://www.apache.org/" , String.class);
ResponseEntity<String> entity = futureEntity.get();

其他类似的不一一列举。

 

2. 异步接口只有Callback形式时

但如果异步接口只能设置自定义Callback函数,不返回Future呢? 比如Thrift就是这样。

Callback函数在这种并行调用场景里并不好用,因此建议还是要转换回Future接口使用。

转换的方法很简单,自己实现一个默认的Callback类,里面包含一个Future,然后实现Callback接口所定义的onSucess()和onFail()函数,将结果或异常赋值到Future中。

DefaultFutureCallback<Long> callback=new DefaultFutureCallback<Long>();
myService.getPrice(1L, callback);
String result = callback.getFuture().get();

至于Future类的实现,随便抄个HttpClient的BasicFuture就好了。

题外话,Future.get() 接口只声明了ExecutionException一种异常,如果你原来的callback函数里有其他不是Runtime Exception的,就要裹在ExecutionException里了,用户还要自己getCause()把它找出来,唯一不方便的地方。

 

3. 对付同步接口

同步接口如JDBC与Jedis,只能在调用它们的地方实现Callable接口,异步的跑在另一个线程池里来完成并行调度。但这其实引入了线程调度的消耗,不得而为之,不可滥用。

Future<Product> future = executor.submit(new Callable<Product>(){
  @Override
  public Product call() throws Exception {
    return productDao.query(id);
  }
});

题外话,executor中返回的Future的实现类叫FutureTask,它能以“Callable 或 Runnable+预设结果(因为Runnalbe自身没结果)“作为参数构建。executor.submit() 接受“Callable 或 Runnable+预设结果” 做参数,内部构建这个FutureTask。但FutureTask本身又是个Runnable,网上有些例子让大家自己在外部把Callable构造成FutureTask,再以Runnable的身份传给executor,其实不好,也没必要。

如果是JDK8,用Lambda就可以写得短一些,只要一行,和平时同步写法也差不多了。

Future<Product> future = executor.submit(()->productDao.query(id));

最后,同步改异步后,原来存在ThreadLocal中的东西如TraceId就没有了,要用一个Context之类的在进出的时候复制。
 
《浅入浅出,谈谈服务化体系中的异步(上)》,转载请保留链接

有关的...

This entry was posted in 技术 and tagged . Bookmark the permalink.

17 Responses to 谈谈服务化体系中的异步(上)

  1. sunjunblack says:

    我能说我是等待白衣大神文章的美女图吗

  2. Pingback: 浅入浅出,谈谈服务化体系中的异步(上) | 神刀安全网

  3. 匿名 says:

    真好奇这么多的美女图源是哪里

    • 匿名 says:

      小伙子代码能力那么好,肯定平时穿女装,所以有那么多女装图

  4. 北京小王 says:

    我是您的粉丝,可以给个联系方式么?我们在用Spring Side,遇到技术难题了,I love you。

  5. afred says:

    对付同步接口,这种方案为了防止线程撑爆或者线程太少导致延迟,一般要怎么调整线程池大小?

  6. 匿名 says:

    博主有品味!张张耐看,我不会告诉你我都翻着看了一遍!

  7. 匿名 says:

    爱上了博主,怎么破

  8. Yang says:

    博主,你只讲了第一层心法,期待你发第二层和第三层呀

    • calvin says:

      一直不得空

      • Yang says:

        目前开发就遇到了依赖的外部资源很难(不可能)异步化的问题,比如RPC接口没有提供异步的,数据库访问、缓存访问没有异步的接口。当要做服务编排的适合,基本只能用同步的模式开发,唯一能优化的也就通过Future延后阻塞的时间点而已。
        看了好些文章,有建议全面reactive化的(感觉特别难),有用Golang作为代理来间接的异步化方案,有建议直接用Quasar协程,还有的建议用Akka actor来封装业务逻辑里的阻塞资源。但貌似Quasar和Akka都还是怕遇到阻塞的访问。个人感觉当阻塞访问依赖的系统资源有限的情况下,比如DB connection pool,thread pool,其他地方再异步,瓶颈还是在这些阻塞访问上。不知道您感觉该怎么解决。

        • calvin says:

          你关注的问题,中篇,和下篇会写,一致抽不到空。

  9. alex says:

    还有机会看到中下篇么

  10. 匿名 says:

    还有机会看到中下篇么

  11. sununiq says:

    中篇和下篇啥时候出啊?

  12. 鲁小憨 says:

    这个问题我一直在思考,现在感觉 Java 8 开始出现的 CompletableFuture 或许是个答案。
    在这一思想下,我搞了一个新的轮子:
    https://github.com/hank-whu/turbo-rpc

发表评论

您的电子邮箱不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>