技术

mosn有的没的 负载均衡泛谈 《Mysql实战45讲》笔记 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go细节 codereview mat使用 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes自动扩容缩容 神经网络模型优化 直觉上理解机器学习 knative入门 如何学习机器学习 神经网络系列笔记 TIDB源码分析 《阿里巴巴云原生实践15讲》笔记 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 副本一致性 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全机制 jvm crash分析 Prometheus 学习 容器日志采集 Kubernetes 控制器模型 Kubernetes监控 容器狂占cpu怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes objects 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言的动态性 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 分布式计算系统的那些套路 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS2——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 硬件对软件设计的影响 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记2 《mysql技术内幕》笔记1 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 Scala的一些梗 OpenTSDB 入门 spring事务小结 事务一致性 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker中涉及到的一些linux知识 hystrix学习 Linux网络源代码学习——整体介绍 zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 Redis概述 机器学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM内存与执行 git spring rmi和thrift maven/ant/gradle使用 再看tcp 缓存系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 VPN(Virtual Private Network) Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go学习 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 kubernetes crd 及kubebuilder学习 pv与pvc实现 csi学习 client-go学习 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 《推荐系统36式》笔记 资源调度泛谈 系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 《聊聊架构》 书评的笔记 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 用户登陆 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列

标签


不同层面的异步

2017年05月16日

简介

如何定义一个系统的性能,参见性能调优攻略

Servlet 3.0 实战:异步 Servlet 与 Comet 风格应用程序

2018.9.26补充,《左耳听风》提到:异步系统主要通过消息队列来对请求做排队处理,把调用方请求的“峰值”给削平了,后端通过自己能够处理的速度来处理请求。这样会增加系统的吞吐量,但实时性就差很多。同时还会引入消息丢失的问题,所以要对消息做持久化,这会造成“有状态”的节点,从而增加服务调度的难度。

2019.1.24补充:异步实现绝大多数时候离不开队列和回调,刚好对应操作系统的 task_struct[] 和 中断处理

异步的价值

为什么异步web可以提高吞吐量

首先,异步不是突然出现一个异步就牛了,而是一系列手段加持的结果

  1. 长连接
  2. Web 线程不需要同步的、一对一的处理客户端请求,能做到一个 Web 线程处理多个客户端请求。同步的实质是线程数限制了连接数,假设tomcat有100个线程,某个请求比较耗时,那么第101个请求就无法创建连接。

举个例子,一个请求建立连接要1s,请求处理要1s,每秒到达100个请求。

  同步 异步
第1s 100个请求连接成功 100个请求连接成功
第2s 100个请求处理成功 100个请求连接成功,100个请求处理成功
第3s 100个请求连接成功 100个请求连接成功,100个请求处理成功
第4s 100个请求处理成功 100个请求连接成功,100个请求处理成功
4s小计 处理200个请求,50qps 100个请求连接成功,300个请求处理成功,65qps

三高

深入剖析通信层和 RPC 调用的异步化(上) 值得读N遍。

传统同步阻塞通信(BIO)面临的主要问题如下:

  1. 性能问题:一连接一线程模型导致服务端的并发接入数和系统吞吐量受到极大限制。
  2. 可靠性问题:由于 I/O 操作采用同步阻塞模式,当网络拥塞或者通信对端处理缓慢会导致 I/O 线程被挂住,阻塞时间无法预测。
  3. 可维护性问题:I/O 线程数无法有效控制、资源无法有效共享(多线程并发问题),系统可维护性差。

《从0开始学架构》笔记 提到设计任何架构都要考虑“三高”,由此看,使用异步也是“三高”的需要。

线程资源是系统中非常重要的资源,在一个进程中线程总数是有限制的,提升线程使用率就能够有效提升系统的吞吐量,在同步 RPC 调用中,如果服务端没有返回响应,客户端业务线程就会一直阻塞,无法处理其它业务消息。

应用场景

深入剖析通信层和 RPC 调用的异步化(下)

  1. 缩短长流程的调用时延
  2. 服务调用耗时波动较大场景。RPC 调用的超时时间配置是个比较棘手的问题。如果配置的过大,一旦服务端返回响应慢,就容易把客户端挂死。如果配置的过小,则超时失败率会增加。通过异步 RPC 调用,就不用再担心调用方业务线程被阻塞,超时时间可以相应配置大一些,减少超时导致的失败。
  3. 第三方接口调用,对于大部分的第三方服务调用,都需要采用防御性编程,防止因为第三方故障导致自身不能正常工作。Hystrix 的第三方故障隔离就是采用类似机制,只不过它底层创建了线程池,通过 Hystrix 的线程池将第三方服务调用与业务线程做了隔离,实现了非侵入式的故障隔离。此处,“通过线程池实现调用线程和业务线程隔离”的表述很有感觉。
  4. 性能和资源利用率提升,尤其针对io密集型的rpc服务,所谓的效率提升就是从io等待的时间中挖来的

异步编程的在各个层面的支持

  特性 效果 备注
操作系统      
NIO 利用 Selector 的轮询以及 I/O 操作的非阻塞特性 使用更少的 I/O 线程处理更多的客户端连接  
Netty NIO + 队列 + 线程池 异步 + io回调 单纯NIO还是同步IO(只是说进行了IO复用),Netty在语言层实现了操作系统层的AIO效果
rpc   异步 + 业务回调  
业务层,比如Java8 的 CompletableFuture   对多个异步操作结果进行逻辑编排  

业务代码

public class Client{
	private Executor executor;
	Future<String> hello(final String name){
		Future<String> future = executor.subimit(new Callable<String>(){
            public String call(){
                return "hello " + name;
            }
        });
		return future;
	}
}

通过聚合一个Executor, 外部调用时,client就可以给人支持异步接口的感觉

Future<String> future = client.hello();

纯异步接口通过添加listener 或callback,也能构成复杂的业务逻辑。

public class Busienss{
	private Client client;
	public Future<String> greet(String name){
		Future<String> future = new xx;
		Future<String> clientFuture = client.hello(name);
		clientFuture.addSuccessListener(new onSuccess(String clientResult){
			String result = handle(clientResult);
			future.set(result)
		});
		return future;

	}
	private String handle(String clientResult){
		return clientResult + ", greet you";
	}
}

操作系统和语言层面

强烈推荐这篇文章有关异步编程框架的讨论,基本要点:

  1. 操作系统就像一个大型的中断处理库。cpu响应中断,运行中断处理程序。操作系统为了最大化利用cpu,一个进程在等待I/O时,另一个进程可以利用CPU进行计算。因此,进行阻塞操作时,进程会执行Schedule让出对cpu的控制权(goroutine也是如此)。

     while(true){
         执行任务 // 因为时间片限定,执行时间不长
         执行任务期间,有中断{
             保存上下文
             执行中断处理程序
         }
         如果任务完毕,结束
     }
    
  2. 虽然知道计算机组成原理,但还是经常误以为计算机只有cpu可以“驱动”逻辑。实际上,cpu触发网络io操作后,自有网卡当组件负责实际的io读写,并在结束时通知cpu。类比下文的rpc框架实现的话,cpu是业务线程,网卡是io线程。从这个角度看,上层和底层实现异曲同工。
  3. 从业务方线程的立场看,肯定想一直占用cpu。但如果线程直接执行的代码中,调用了可能阻塞的系统调用,失去cpu就在所难免。

    • golang直接实现了一个调度器,碰上阻塞操作(重写了阻塞操作的逻辑,不会直接调用系统调用),挂起的是goroutine,实际的物理线程执行下调度程序,找下一个goroutine接着跑。
    • python twisted, Java Netty, Nodejs libuv 这些框架没有深入到语言层,没办法推翻重来(r/w或者nio的select操作还是得调用系统调用),而是做一个自己的事件驱动引擎。
  4. 事件驱动引擎。业务操作分为阻塞操作(差不多就是io操作)和非阻塞操作

    • 通过重构整体逻辑,自由控制阻塞和非阻塞操作比例。比如netty eventloop中有io ratio
    • 将阻塞操作和非阻塞操作 分派给不同的线程池来执行,甚至io操作分门别类给不同的线程池来执行,比如netty中的boss和worker。
  5. 异步编程与顺序编程差异非常大,代码要重新组织,将业务逻辑分派在不同的线程中(有点分布式系统中把任务分发到不同node的感觉,每个node上也跑了一个任务执行引擎),并对中间结果进行组合/编排。

文章最后小结提到:其实从某种程度来说,异步框架是程序试图跳出操作系统界定的同步模型,重新虚拟出一套执行机制,让框架的使用者看起来像一个异步模型。另外通过把很多依赖操作系统实现的笨重功能换到程序内部使用更轻量级的实现。 全面异步化:淘宝反应式架构升级探索 提到:异步化会将操作系统中的队列情况显式地提升到了应用层,使得应用层可以显式根据队列的情况来进行压力负载的感知。

通信层面——以netty 为例

netty中的线程池

2018.6.30 补充。拿netty 和 go 类比一下,可以看到,调用go语言的阻塞方法(io方法不确定是不是这样),相当于

read(){
	保存上下文
	让出goroutine 执行权
}

go 中实际的 阻塞 操作实际是go语言层完成的,goroutine 调度本身是对 内核系统调用的模仿。只是软中断变成了方法调用,线程阻塞改成 goroutine 阻塞。

netty 因为不能改写 io 语言的系统调用,为此 不敢向你直接暴露 io 方法,封装了一个channel 出来,你调用channel.write() 不是真正的java.nio.channels.SocketChannel.write。 所以netty 也只能提供 一个 单独的层次,与上图竖着画驱动线程与事件驱动引擎(线程)的关系不同

相当于netty 提供了一个全异步 io 操作(也包括一般的任务执行)的抽象层,支持类似AIO的”系统调用“。所以上图竖着画,就是另一番滋味了。

rpc 层

RPC 框架异步调度模型

异步 RPC 调用的关键点有 2 个:

  1. 不能阻塞调用方线程
  2. 请求和响应的上下文关联,大部分二进制协议的 TCP 链路都是多路复用的,请求和响应消息的发送和接收顺序是无序的。所以,异步 RPC 调用需要缓存请求和响应的上下文关联关系,以及响应需要使用到的消息上下文。

请求和响应的上下文缓存 相当于rpc 层与io 通信层的 “缓冲层”,双方各干各的。

rpc层 的异步实现

  1. 直接使用异步框架,异步直接反应在接口上。Future/Promise,比较常用的有 JDK8 之前的 Future,通过添加 Listener 来做异步回调,JDK8 之后通常使用 CompletableFuture,它支持各种复杂的异步处理策略,例如自定义线程池、多个异步操作的编排、有返回值和无返回值异步、多个异步操作的级联操作等。

  2. 业务逻辑不动,异步分装。线程池 +RxJava,最经典的实现就是 Netflix 开源的 Hystrix 框架,使用 HystrixCommand(创建线程池)做一层异步封装,将同步调用封装成异步调用,利用 RxJava API,通过订阅的方式对结果做异步处理

异步化在工程上的推进

深入剖析通信层和 RPC 调用的异步化(下)

  io通信层 业务处理
tomcat nio/bio servlet
rpc server base on netty nio xxHandler

不同层次的同步异步之间,没有必然关系。future 因为分层之间本质关联的是数据(回调的本质是下层给上层传递数据),不是数据的获取方式。异步也是要拿到结果的,这一点与同步没有差异

tomcat 和 异步servlet

Tomcat 支持 NIO,与 Tomcat 的 HTTP 服务是否是异步的,没有必然关系

  1. HTTP 消息的读写:即便采用了 NIO,HTTP 请求和响应的消息处理(也就是业务处理部分)仍然可能是同步阻塞的,这与协议栈的具体策略有关系。
  2. 每个响应对象只有在 Servlet 的 service 方法或 Filter 的 doFilter 方法范围内有效,该方法一旦调用完成,Tomcat 就认为本次 HTTP 消息处理完成,它会回收 HttpServletRequest 和 HttpServletResponse 对象再利用,如果业务异步化之后再处理 HttpServletResponse,拿到的实际就不是之前请求消息对应的响应,会发生各种非预期问题

如果使用的是支持 Servlet3.0+ 版本的 Tomcat,可以开启异步处理模式。Servlet3 之前一个 HTTP 请求消息的处理流程,包括:HTTP 请求消息的解析、Read Body、Response Body,以及后续的业务逻辑处理都是由 Tomcat 线程池中的工作线程处理Servlet3 之后可以让 I/O 线程和业务处理线程分开,进而对业务做隔离和异步化处理。还可以根据业务重要性进行业务分级,同时把业务线程池分类,实现业务的优先级处理,隔离核心业务和普通业务,提升应用可靠性。PS: 从这个角度,tomcat异步实现与rpc框架实现是一样一样的

Servlet3.1 以后增加了对非阻塞 I/O 的支持,根据 Servlet3.1 规范中描述:非阻塞 I/O 仅对在 Servlet 中的异步处理请求有效。Servlet3.1 对非阻塞 I/O 的支持是对之前异步化版本的增强,配套 Tomcat8.X 版本。

springmvc 异步化

SpringMVC 支持多种异步化模式,常用的有两种:

  1. Controller 的返回值为 DeferredResult, Controller 方法内构造 DeferredResult 对象,然后将请求封装成 Task 投递到业务线程池中异步执行,业务执行完成之后,构造 ModelAndView,调用 deferredResult.setResult(ModelAndView) 完成异步化处理和响应消息的发送。

  2. Controller 的返回值为 WebAsyncTask,实现 Callable, 在 call 方法中完成业务逻辑处理,由 SpringMVC 框架的线程池来异步执行业务逻辑(非 Tomcat 工作线程)。

rpc 框架异步化

RPC 异步与 I/O 的异步没有必然关系,当 RPC 调用请求消息发送到 I/O 线程的消息队列之后,业务线程就可以返回,至于 I/O 线程采用同步还是异步的方式读写消息,与 RPC 调用的同步和异步没必然的关联关系。当然,在大多数场景下,RPC 框架底层会使用异步 I/O,实现全栈异步, 整体性能和可靠性会更好一些。

Apache ServiceComb/gRPC

技术难点

  1. 异步异常传递
  2. 超时控制
  3. 上下文传递,传统的同步 RPC 调用时,业务往往通过线程变量来传递上下文,例如:TraceID、会话 Session、IP 等信息。
  4. 回调地狱问题,如果使用的是 JDK8 的 CompletableFuture,它支持对异步操作结果做编排以及级联操作

小结

异步为什么性能更好?

  1. 在开发层面给予更多选择,可以不让线程让出cpu。开发者往往比操作系统知道怎样更高效利用线程。这也是java、netty中一脉相承的思路,比如AQS、CAS来减少线程切换,common-pool、netty中的arena手动管理内存。
  2. 事件驱动引擎是一种编程模型,可以不处理io事件。但加入io事件的处理后,select集中等待,并可以在io任务和cpu任务中控制开销比例,进而可以做到更高效。

引用

性能调优攻略