如何提高网站的吞吐量

吞吐量定义百科 吞吐量是指对网络、设备、端口、虚电路或其他设施,单位时间内成功地传送数据的数量(以比特、字节、分组等测量)。以上的定义比较宽泛,定义到网站或者接口的吞吐量是这样的:吞吐量是指系统在单位时间内处理请求的数量。这里有一个注意点就是单位时间内,对于网站的吞吐量这个单位时间一般定义为1秒,也就是说网站在一秒之内能处理多少http(https/tcp)请求。与吞吐量对应的衡量网站性能的还有响应时间、并发数、QPS每秒查询率。 响应时间是一个系统最重要的指标之一,它的数值大小直接反应了系统的快慢。响应时间是指执行一个请求从开始到最后收到响应数据所花费的总体时间。并发数是指系统同时能处理的请求数量,这个也是反应了系统的负载能力。 每秒查询率(QPS)是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。对应fetches/sec,即每秒的响应请求数,也即是最大吞吐能力。 我们以高速收费站为例子也许更直观一些,吞吐量就是一天之内通过的车辆数,响应时间就是车速,并发数就是高速上同时奔跑的汽车数。由此可见其实以上几个指标是有内在联系的。比如:响应时间缩短,在一定程度上可以提高吞吐量。 其实以上几个指标主要反映了两个概念: 系统在单位时间之内能做多少事情系统做一件事情需要的时间提高吞吐量以下场景都是在假设程序不发生异常的情况下服务器(进程)级别服务器级别增加网站吞吐量也是诸多措施中最容易并且是效果最好的,如果一个网站能通过增加少量的服务器来提高吞吐量,菜菜觉得是应该优先采用的。毕竟一台服务器的费用相比较一个程序员费用来说要低的多。但是有一个前提,就是你的服务器是系统的瓶颈,网站系统之后的其他系统并非瓶颈。如果你的系统的瓶颈在DB或者其他服务,盲目的增加服务器并不能解决你的问题。 通过增加服务器来解决你的网站瓶颈,意味着你的网站需要做负载均衡,如果没有运维相关人员,你可能还得需要研究负载均衡的方案,比如LVS,Nginx,F5等。我曾经面试过很多入道不久的同学,就提高吞吐量问题,如果没有回答上用负载均衡方案的基本都pass了,不要说别的,这个方案就是一个基础,就好比学习一个语言,你连最基本的语法都不会,我凭什么让你通过。有喷的同学可以留言哦 其实现在很多静态文件采用CDN,本质上也可以认为是增加服务器的策略线程级别当一个请求到达服务器并且正确的被服务器接收之后,最终执行这个请求的载体是一个线程。当一个线程被cpu载入执行其指令的时候,在同步的状态下,当前线程会阻塞在那里等待cpu结果,如果cpu执行的是比较慢的IO操作,线程会一直被阻塞闲置很长时间,这里的很长是对比cpu的速度而言,如果你想有一个直观的速度对比,可以去查看菜菜以前的文章:高并发下为什么更喜欢进程内缓存当一个新的请求到来的时候,如果没有新的线程去领取这个任务并执行,要么会发生异常,要么创建新的线程。线程是一种很稀缺的资源,不可能无限制的创建。这种情况下我们就要把线程这种资源充分利用起来,不要让线程停下来。这也是程序推荐采用异步的原因,试想,一个线程不停的在工作,遇到比较慢的IO不会去等待结果,而是接着处理下一个请求,当IO的结果返回来得到通知的时候,线程再去取IO结果,岂不是能在相同时间内处理更多的请求。 程序异步化(非阻塞)会明显提高系统的吞吐量,但是响应时间可能会稍微变大还有一点,尽量减少线程上线文在cpu的切换,因为线程上线文切换的成本也是比较大的,在线程切换的时候,cpu需要把当前线程的上下文信息记录下来用以下次调用的时候使用,然后把新线程的上下文信息载入然后执行。这个过程相对于cpu的执行速度而言,要慢很多。 不要拿Golang反驳以上观点,golang的协程虽然是用户级别比线程更小的载体,但是最终和Cpu进行交互的还是线程。 Cpu级别在讲cpu级别之前,如果有一定的网络模型的基础,也许会好一些。这里大体阐述一下,现代操作系统都采用虚拟寻址的方式,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统将虚拟空间分为两类:内核空间和用户空间。内核空间独立于用户空间,有访问受保护的内存空间、IO设备的权限(所有的用户空间共享)。用户空间就是我们的应用程序运行的空间,其实用户空间并没有操作各种IO设备的权限,像我们平时读取一个文件,本质上是委托内核空间去执行读取指令的,内核空间读取到数据之后再把数据复制到程序运行的空间,最后应用程序再把数据返回调用方。 通过上图大体可以看出,内核会为每个I/O设备维护一个buffer(同一个文件描述符读和写的buffer不同),应用程序发出一个IO操作的指令其实通过了内核空间和用户空间两个部分,并且发生了数据的复制操作。这个过程其实主要包含两个步骤: 用户进程发出操作指令并等待数据内核把数据返回给用户进程(buffer的复制操作)根据这两个操作的不同表现,所以IO模型有了同步阻塞,同步非阻塞,异步阻塞,异步非阻塞的概念,但是这里并非此文的重点,所以不在展开详细介绍。 利用cpu提高系统吞吐量主要目标是提高单位时间内cpu运行的指令数,避免cpu做一些无用功: cpu负责把buffer的数据copy到应用程序空间,应用程序再把数据返回给调用方,假如这个过程发生的是一次Socket操作,应用程序在得到IO返回数据之后,还需要网卡把数据返回给client端,这个过程又需要把刚刚得到的buffer数据再次通过内核发送至网卡,通过网络传送出去。由此可见cpu把buffer数据copy到应用程序空间这个过程完全没有必要,在内核空间完全可以把buffer数据直接传输至网卡,这也是零拷贝技术要解决的问题。具体的零拷贝技术在这里不再展开。不要让任何设备停下来,不要让任何设备做无用功通过增加cpu的个数来增加吞吐量 网络传输级别至于网络传输级别,由于协议大部分是Tcp/ip,所以在协议传输方面优化的手段比较少,但是应用程序级别协议可以选择压缩率更好的,比如采用grpc会比单纯的http协议要好很多,http2 要比http 1.1要好很多。另外一方面网卡尽量加大传输速率,比如千兆网卡要比百兆网卡速度更快。由于网络传输比较偏底层,所以人工干预的切入点会少很多。 最后总结大部分程序员都是工作在应用层,针对应用级别代码能提高吞吐量的建议: 加大应用的进程数,增加并发数,特别在进程数是瓶颈的情况下优化线程调用,尽量池化。应用的代码异步化,特别是异步非阻塞式编程对于提高吞吐量效果特别明显充分利用多核cpu优势,实现并行编程。减少每个调用的响应时间,缩短调用链。例如通过加索引的方式来减少访问一次数据库的时间搜索公众号:架构师修行之路,领取福利,获取更多精彩内容

October 13, 2019 · 1 min · jiezi

Java并发24并发设计模式-生产者消费者模式并发提高效率

生产者 - 消费者模式在编程领域的应用非常广泛,前面我们曾经提到,Java 线程池本质上就是用生产者 - 消费者模式实现的,所以每当使用线程池的时候,其实就是在应用生产者 - 消费者模式。 当然,除了在线程池中的应用,为了提升性能,并发编程领域很多地方也都用到了生产者 - 消费者模式,例如 Log4j2 中异步 Appender 内部也用到了生产者 - 消费者模式。所以我们就来深入地聊聊生产者 - 消费者模式,看看它具体有哪些优点,以及如何提升系统的性能。 生产者 - 消费者模式的优点生产者 - 消费者模式的核心是一个任务队列,生产者线程生产任务,并将任务添加到任务队列中,而消费者线程从任务队列中获取任务并执行。下面是生产者 - 消费者模式的一个示意图,你可以结合它来理解。 生产者 - 消费者模式示意图#### 从架构设计的角度来看,生产者 - 消费者模式有一个很重要的优点,就是解耦。解耦对于大型系统的设计非常重要,而解耦的一个关键就是组件之间的依赖关系和通信方式必须受限。在生产者 - 消费者模式中,生产者和消费者没有任何依赖关系,它们彼此之间的通信只能通过任务队列,所以生产者 - 消费者模式是一个不错的解耦方案 除了架构设计上的优点之外,生产者 - 消费者模式还有一个重要的优点就是支持异步,并且能够平衡生产者和消费者的速度差异。在生产者 - 消费者模式中,生产者线程只需要将任务添加到任务队列而无需等待任务被消费者线程执行完,也就是说任务的生产和消费是异步的,这是与传统的方法之间调用的本质区别。 异步化处理最简单的方式就是创建一个新的线程去处理,那中间增加一个任务队列”究竟有什么用呢?主要还是用于平衡生产者和消费者的速度差异。我们假设生产者的速率很慢,而消费者的速率很高,比如是 1:3,如果生产者有 3 个线程,采用创建新的线程的方式,那么会创建 3 个子线程,而采用生产者 - 消费者模式,消费线程只需要 1 个就可以了。 Java 语言里,Java 线程和操作系统线程是一一对应的,线程创建得太多,会增加上下文切换的成本,所以 Java 线程不是越多越好,适量即可。 支持批量执行以提升性能在两阶段终止模式:优雅地终止线程中,我们提到一个监控系统动态采集的案例,其实最终回传的监控数据还是要存入数据库的(如下图)。但被监控系统往往有很多,如果每一条回传数据都直接 INSERT 到数据库,那DB压力就非常大了。很显然,更好的方案是批量执行 SQL,那如何实现呢?这就要用到生产者 - 消费者模式了。 动态采集功能示意图 #### 利用生产者 - 消费者模式实现批量执行 SQL 非常简单:将原来直接 INSERT 数据到数据库的线程作为生产者线程,生产者线程只需将数据添加到任务队列,然后消费者线程负责将任务从任务队列中批量取出并批量执行。 ...

July 15, 2019 · 2 min · jiezi

Java并发20并发设计模式-Guarded-Suspension模式等待唤醒机制的规范实现

在开发中我们或许回遇到这样的情况:有一个Web 版的文件浏览器,通过它用户可以在浏览器里查看服务器上的目录和文件。这个项目依赖运维部门提供的文件浏览服务,而这个文件浏览服务只支持消息队列(MQ)方式接入。消息队列在互联网大厂中用的非常多,主要用作流量削峰和系统解耦。在这种接入方式中,发送消息和消费结果这两个操作之间是异步的,你可以参考下面的示意图来理解。 消息队列(MQ)示意图 在这个 Web 项目中,用户通过浏览器发过来一个请求,会被转换成一个异步消息发送给 MQ,等 MQ 返回结果后,再将这个结果返回至浏览器。问题来了:给 MQ 发送消息的线程是处理 Web 请求的线程 T1,但消费 MQ 结果的线程并不是线程 T1,那线程 T1 如何等待 MQ 的返回结果呢?示例代码如下。 class Message{ String id; String content;}// 该方法可以发送消息void send(Message msg){ // 省略相关代码}//MQ 消息返回后会调用该方法// 该方法的执行线程不同于// 发送消息的线程void onMessage(Message msg){ // 省略相关代码}// 处理浏览器发来的请求Respond handleWebReq(){ // 创建一消息 Message msg1 = new Message("1","{...}"); // 发送消息 send(msg1); // 如何等待 MQ 返回的消息呢? String result = ...;}Guarded Suspension 模式上面遇到的问题,在现实世界里比比皆是,只是我们一不小心就忽略了。比如,项目组团建要外出聚餐,我们提前预订了一个包间,然后兴冲冲地奔过去,到那儿后大堂经理看了一眼包间,发现服务员正在收拾,就会告诉我们:“您预订的包间服务员正在收拾,请您稍等片刻。”过了一会,大堂经理发现包间已经收拾完了,于是马上带我们去包间就餐。 我们等待包间收拾完的这个过程和前面的等待 MQ 返回消息本质上是一样的,都是等待一个条件满足:就餐需要等待包间收拾完,程序里要等待 MQ 返回消息。 ...

July 9, 2019 · 3 min · jiezi

理解Java中的Volatile关键字demo

什么是volatile 关键字volatile 提供了Java 虚拟机中最轻量级的同步机制。在meidium 中有篇文章说:Volatile specifier is used to indicate that a variable’s value can be modified by multiple threads simultaneously 当你使用了volatile之后,那么这个变量会有如下的特性: 保证此变量对所有的线程的可见性禁止指令重排序优化对于第一条的内容,volatile是如何保证读写操作对所有的线程可见? 这里涉及到可见性的问题。 对于普通的变量来说,每个线程要想修改变量的值,首先从主存(Main Mermory, 也就是我们通常所说的)中拷贝值到自己所属的CPU cache中去。现在的处理器,一般是会有多个CPU的,每个线程可能运行在不同的CPU, 那么线程修改完成的值,是首先保存在CPU cache中去。此时其他的线程是察觉不到修改的结果的,这就造成了并发模型中的可见性问题。 考虑这样一种情况: public class SharedObject { public int counter = 0;}现在有两个线程, 线程1和线程2, 他们不时会去读取这个共享变量。如果counter 没有声明volatile这个关键字,那么此时就无法保证counter的值何时从CPU缓存写回到主存。这意味着,counter在CPU cache中的值和主存中的会不一致。 但是如果声明了volatile的话,对volatile变量进行写操作的话,那么JVM就会向处理器发送一条LOCK前缀的指令,将这个变量所在缓存行中的数据写回到系统内存。 写回的过程,为了保证各个处理器的缓存是一一致的,就会实现缓存一致性协议。 缓存不停地窥探总线上发生的数据交换,来检查自己的数据是不是已经过期了。如果发现此时缓存行中的值已经失效的话,就会将当前缓存行中的值设置为无效状态。处理器要用到这个值的话,就会从内存中重新获取这个值。 禁止指令重排序 指令重排序是指CPU采用了允许将多条指令不按照程序规定的顺序分开发送给各相应的电路单元处理 参考: https://blog.usejournal.com/j...https://medium.com/@siddhusin...https://www.cnblogs.com/dolph...《Java特种兵》

July 7, 2019 · 1 min · jiezi

Week-1-Java-多线程-Java-内存模型

前言学习情况记录 时间:week 1SMART子目标 :Java 多线程学习Java多线程,要了解多线程可能出现的并发现象,了解Java内存模型的知识是必不可少的。 对学习到的重要知识点进行的记录。 注:这里提到的是Java内存模型,是和并发编程相关的,不是JVM内存结构(堆、方法栈这些概念),这两个不是一回事,别弄混了。 Java 内存模型Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能得到一致效果的机制及规范。目的是解决由于多线程通过共享内存进行通信时,存在的原子性、可见性(缓存一致性)以及有序性问题。主内存与工作内存先看计算机硬件的缓存访问操作: 处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。 加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。 Java的内存访问操作与上述的硬件缓存具有很高的可比性: Java内存模型中,规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。 内存间交互操作Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作 read:把一个变量的值从主内存传输到线程的工作内存中load:在 read 之后执行,把 read 得到的值放入线程的工作内存的变量副本中use:把线程的工作内存中一个变量的值传递给执行引擎assign:把一个从执行引擎接收到的值赋给工作内存的变量store:把工作内存的一个变量的值传送到主内存中write:在 store 之后执行,把 store 得到的值放入主内存的变量中lock:作用于主内存的变量,把一个变量标识成一条线程独占的状态unlock: 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。内存模型三大特性原子性Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,也就是说基本数据类型的访问读写是原子性的,除了long和double是非原子性的,即 load、store、read 和 write 操作可以不具备原子性。书上提醒我们只需要知道有这么一回事,因为这个是几乎不可能存在的例外情况。 虽然上面说对基本数据类型的访问读写是原子性的,但是不代表在多线程环境中,如int类型的变量不会出现线程安全问题。详细的例子可以参考范例一。 想要保证原子性,可以尝试以下几种方式: 如果是基础类型的变量的话,使用Atomic类(例如AtomicInteger)其他情况下,可以使用synchronized互斥锁来保证 限定临界区 内操作的原子性。它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。可见性可见性指的是,当一个线程修改了共享变量中的值,其他线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。 ...

July 7, 2019 · 2 min · jiezi

Java并发18并发设计模式-COW模式CopyonWrite模式的应用领域

在上一篇文章中我们讲到 Java 里 String 这个类在实现 replace() 方法的时候,并没有更改原字符串里面 value[] 数组的内容,而是创建了一个新字符串,这种方法在解决不可变对象的修改问题时经常用到。如果你深入地思考这个方法,你会发现它本质上是一种Copy-on-Write 方法。所谓 Copy-on-Write,经常被缩写为 COW 或者 CoW,顾名思义就是写时复制。 不可变对象的写操作往往都是使用 Copy-on-Write 方法解决的,当然 Copy-on-Write 的应用领域并不局限于 Immutability 模式。下面我们先简单介绍一下 Copy-on-Write 的应用领域,让你对它有个更全面的认识。 Copy-on-Write 模式的应用领域我们知道 CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器,它们背后的设计思想就是 Copy-on-Write;通过 Copy-on-Write 这两个容器实现的读操作是无锁的,由于无锁,所以将读操作的性能发挥到了极致。 除了上面我们说的 Java 领域,很多其他领域也都能看到 Copy-on-Write 的身影:Docker 容器镜像的设计是 Copy-on-Write,甚至分布式源码管理系统 Git 背后的设计思想都有 Copy-on-Write。 CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器在修改的时候会复制整个数组,所以如果容器经常被修改或者这个数组本身就非常大的时候,是不建议使用的。反之,如果是修改非常少、数组数量也不大,并且对读性能要求苛刻的场景,使用 Copy-on-Write 容器效果就非常好了。 一个真实案例RPC 框架中有个基本的核心功能就是负载均衡。服务提供方是多实例分布式部署的,所以服务的客户端在调用 RPC 的时候,会选定一个服务实例来调用,这个选定的过程本质上就是在做负载均衡,而做负载均衡的前提是客户端要有全部的路由信息。 例如在下图中,A 服务的提供方有 3 个实例,分别是 192.168.1.1、192.168.1.2 和 192.168.1.3,客户端在调用目标服务 A 前,首先需要做的是负载均衡,也就是从这 3 个实例中选出 1 个来,然后再通过 RPC 把请求发送选中的目标实例。 ...

July 4, 2019 · 2 min · jiezi

Java并发17并发设计模式-Immutability模式如何利用不变性解决并发问题

解决并发问题,其实最简单的办法就是让共享变量只有读操作,而没有写操作。这个办法如此重要,以至于被上升到了一种解决并发问题的设计模式:不变性(Immutability)模式。所谓不变性,简单来讲,就是对象一旦被创建之后,状态就不再发生变化。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性。 快速实现具备不可变性的类实现一个具备不可变性的类,还是挺简单的。将一个类所有的属性都设置成 final 的,并且只允许存在只读方法,那么这个类基本上就具备不可变性了。更严格的做法是这个类本身也是 final 的,也就是不允许继承。因为子类可以覆盖父类的方法,有可能改变不可变性,所以推荐你在实际工作中,使用这种更严格的做法。 Java SDK 里很多类都具备不可变性,只是由于它们的使用太简单,最后反而被忽略了。例如经常用到的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。如果你仔细翻看这些类的声明、属性和方法,你会发现它们都严格遵守不可变类的三点要求:类和属性都是 final 的,所有方法均是只读的 我们结合 String 的源代码来解释一下这个问题,下面的示例代码源自 Java 1.8 SDK,我略做了修改,仅保留了关键属性 value[] 和 replace() 方法,你会发现:String 这个类以及它的属性 value[] 都是 final 的;而 replace() 方法的实现,就的确没有修改 value[],而是将替换后的字符串作为返回值返回了。 public final class String { private final char value[]; // 字符替换 String replace(char oldChar, char newChar) { // 无需替换,直接返回 this if (oldChar == newChar){ return this; } int len = value.length; int i = -1; /* avoid getfield opcode */ char[] val = value; // 定位到需要替换的字符位置 while (++i < len) { if (val[i] == oldChar) { break; } } // 未找到 oldChar,无需替换 if (i >= len) { return this; } // 创建一个 buf[],这是关键 // 用来保存替换后的字符串 char buf[] = new char[len]; for (int j = 0; j < i; j++) { buf[j] = val[j]; } while (i < len) { char c = val[i]; buf[i] = (c == oldChar) ? newChar : c; i++; } // 创建一个新的字符串返回 // 原字符串不会发生任何变化 return new String(buf, true); }}通过分析 String 的实现,你可能已经发现了,如果具备不可变性的类,需要提供类似修改的功能,具体该怎么操作呢?做法很简单,那就是创建一个新的不可变对象,这是与可变对象的一个重要区别,可变对象往往是修改自己的属性。 ...

July 4, 2019 · 2 min · jiezi

如何使用jMeter对某个OData服务进行高并发性能测试

For project reason I have to measure the performance of OData service being accessed parallelly. And I plan to use the open source tool JMeter to generate a huge number of request in parallel and measure the average response time. Since I am a beginner for JMeter, I write down what I have learned into this blog. I will continue to explorer the advanced feature of JMeter in my daily work.我们公司某团队开发了一个OData服务,现在我接到任务,要测试这个服务在高并发访问场景下的性能指标,比如5万个请求同时到来后,每个请求的平均响应时间,因此我选择了jMeter这个好用的工具来模拟高并发请求。Download JMeter from its official website:http://jmeter.apache.org/Go to the installation folder, add the following text in file binuser.properties:httpclient4.retrycount=1hc.parameters.file=hc.parametersCreate a new test plan for example Customer_Query_OData_test, and right click on it and create a thread group from context menu.创建一个新的测试plan,基于其再创建一个线程组:Below configuration means I would like to generate three request in parallel via three threads, each thread is executed only once. And there is no delay during the spawn of each threads ( Ramp-Up Period = 0 )下列设置意思是我想创建三个并发请求,每个请求通过一个线程实现,每个线程仅仅执行一次。每个线程派生后的延时是0秒,意思是主线程同时创建三个线程。创建一个新的HTTP请求,维护下列设置:Create a new Http Request and maintain the following settings:(1) Protocol: https(2) Server name:(3) Http request method: GET(4) Http path: /sap/c4c/odata/v1/c4codata/AccountCollection/ - 这就是OData服务的相对路径了(5) Use KeepAlive: do NOT select this checkbox - 记得这个勾别打上In Parameter tab, maintain query option $search with value ‘Wang’这个意思就是每个并发请求同时发起OData查询,参数为我的名字WangSwitch to Advanced tab, choose “HttpClient4” from drop down list for Implementation, and maintain proxy server name and port number.如果有代理的话,在下图位置维护代理服务器信息。Create a new HTTP Header Manager and specify the basic authentication header field and value.在HTTP Header Manager里维护访问这个Odata服务的credential。因为我们开发的OData服务支持Basic Authentication这种认真方式,所以我在此处的HTTP header字段里维护Authentication信息。Create a listener for the test plan. In my test I simply choose the most simple one: View Results in Table.创建listener,主要用途当然是显示测试结果了。我使用的是jMeter自带的Listener,Table类型的,以表格形式显示高并发请求和响应的各项指标。Once done, start the test:一切就绪,点击这个绿色的三角形开始测试:After the test is finished, double click on View Result Listener and the response time for each request and the average response time is displayed there:测试完毕后,双击我们之前创建的Table Result Listener,我这三个并发请求的性能指标就显示出来了。可以看到三个请求中,最快的请求用了5.1秒,最慢的6.9秒当然,jMeter也支持命令行方式使用:Or you can use command line to achieve the same:-n: use non-GUI mode-t: specify which test plan you want to run-l: specify the path of output result file为了检验jMeter采集的数据是否正确可靠,我还花时间写了一个Java程序,用JDK自带的线程池产生并发请求,测试的结果和jMeter是一致的。And I have written a simple Java application to generate parallel request via multiple thread and the result measured in Java program is consistent with the one got from JMeter.The source code could be found from my github:我的Java程序放在我的github上:https://github.com/i042416/Ja…How to generate random query for each thread in JMeter到目前为止,我的三个并发请求进行搜索的参数都是硬编码的Wang,这个和实际场景不太符合。有没有办法生成一些随机的搜索字符串,这样更贴近真实使用场景呢?Suppose we would like each thread in JMeter to generate different customer query via OData with the format JerryTestCustomer_<1~100>, we can simply create a new user parameter:当然有办法:右键菜单,Add->Pre Processors(预处理器)->User Parameters:参数名Parameter name,取为uuid参数值Parameter value: use JMeter predefined function __Random to generate random number. 使用jMeter自带的随机数生成函数__Random。因此最后参数uuid的值为${__Random(1,100)},意思是生成1到100内的随机正整数and in http request, just specify reference to this variable via ${uuid}:在http请求里,用固定的前缀JerryTestCustomer_加上随机参数,以此来构造随机搜索字符串:So that in the end each thread will issue different query to OData service end point.通过Table Result listener,能观察到这次确实每个请求发起的搜索都使用了不同的字符串了。希望这篇文章介绍的jMeter使用技巧对大家工作有所帮助。要获取更多Jerry的原创文章,请关注公众号"汪子熙": ...

January 13, 2019 · 2 min · jiezi

Golang并发模型:select进阶

最近公司工作有点多,Golang的select进阶就这样被拖沓啦,今天坚持把时间挤一挤,把吹的牛皮补上。前一篇文章《Golang并发模型:轻松入门select》介绍了select的作用和它的基本用法,这次介绍它的3个进阶特性。nil的通道永远阻塞如何跳出for-selectselect{}阻塞nil的通道永远阻塞当case上读一个通道时,如果这个通道是nil,则该case永远阻塞。这个功能有1个妙用,select通常处理的是多个通道,当某个读通道关闭了,但不想select再继续关注此case,继续处理其他case,把该通道设置为nil即可。下面是一个合并程序等待两个输入通道都关闭后才退出的例子,就使用了这个特性。func combine(inCh1, inCh2 <-chan int) <-chan int { // 输出通道 out := make(chan int) // 启动协程合并数据 go func() { defer close(out) for { select { case x, open := <-inCh1: if !open { inCh1 = nil continue } out<-x case x, open := <-inCh2: if !open { inCh2 = nil continue } out<-x } // 当ch1和ch2都关闭是才退出 if inCh1 == nil && inCh2 == nil { break } } }() return out}如何跳出for-selectbreak在select内的并不能跳出for-select循环。看下面的例子,consume函数从通道inCh不停读数据,期待在inCh关闭后退出for-select循环,但结果是永远没有退出。func consume(inCh <-chan int) { i := 0 for { fmt.Printf(“for: %d\n”, i) select { case x, open := <-inCh: if !open { break } fmt.Printf(“read: %d\n”, x) } i++ } fmt.Println(“combine-routine exit”)}运行结果:➜ go run x.gofor: 0read: 0for: 1read: 1for: 2read: 2for: 3gen exitfor: 4for: 5for: 6for: 7for: 8… // never stop既然break不能跳出for-select,那怎么办呢?给你3个锦囊:在满足条件的case内,使用return,如果有结尾工作,尝试交给defer。在select外for内使用break挑出循环,如combine函数。使用goto。select{}永远阻塞select{}的效果等价于创建了1个通道,直接从通道读数据:ch := make(chan int)<-ch但是,这个写起来多麻烦啊!没select{}简洁啊。但是,永远阻塞能有什么用呢!?当你开发一个并发程序的时候,main函数千万不能在子协程干完活前退出啊,不然所有的协程都被迫退出了,还怎么提供服务呢?比如,写了个Web服务程序,端口监听、后端处理等等都在子协程跑起来了,main函数这时候能退出吗?select应用场景最后,介绍下我常用的select场景:无阻塞的读、写通道。即使通道是带缓存的,也是存在阻塞的情况,使用select可以完美的解决阻塞读写,这篇文章我之前发在了个人博客,后面给大家介绍下。给某个请求/处理/操作,设置超时时间,一旦超时时间内无法完成,则停止处理。select本色:多通道处理并发系列文章推荐Golang并发模型:轻松入门流水线模型Golang并发模型:轻松入门流水线FAN模式Golang并发模型:并发协程的优雅退出Golang并发模型:轻松入门select如果这篇文章对你有帮助,请点个赞/喜欢,鼓励我持续分享,感谢。我的文章列表,点此可查看如果喜欢本文,随意转载,但请保留此原文链接。 ...

December 18, 2018 · 1 min · jiezi

Java内存模型与线程

对《深入理解Java虚拟机——周志明》中第12章的总结概述硬件效率与一致性 为了解决处理器与内存之间的速度矛盾,引入了基于高速缓存的存储交互。 但高速缓存的引入也带来了新的问题:缓存一致性,即多处理器中,每个处理器有各自的高速缓存,而他们又共享同一主内存。当多个处理器的运算任务额都涉及同一块主存区域的时候,将可能导致各自的缓存数据不一致,如果真的发生这种情况,那么同步回到主存时以谁的缓存数据为准呢? 为了解决一致性的问题,需要各个处理器在访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。在本章中将会多次提到“内存模型”一词,可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问时的过程抽象。不同的物理机器可以拥有不同的内存模型。而Java虚拟机也拥有自己的内存模型,并且在这里的内存访问操作与硬件的访问操作具有很高的可比性。Java内存模型 Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在JDK1.5(实现了JSR-133)发布后,Java内存模型已经成熟和完善起来了。主内存和工作内存 Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。这里的变量不包括局部变量和参数,因为其实线程私有的,不会被共享(但局部引用变量所指向的对象仍然是可共享的),自然不会存在竞争问题。 Java内存模型规定了所有的变量都存储在主内存中(此处的主内存与介绍物理硬件时的主内存名字一样,两者也可以相互类比,但此处仅是虚拟机内存的一部分),每条线程还有自己的工作内存(可与高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量(包括volatile变量也是这样)。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。内存间交互操作 关于主内存和工作内存之间具体的交互协议:即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节。Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double、long类型的变量来说,load、store、read和write操作在某些平台上允许有例外)。lockunlockreadloaduseassignstorewrite 如果要把一个变量从主内存复制到工作内存,那么就要顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作。注意:Java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行。也就是说read和write之间是可以插入其他指令的,如对主内存的变量a、b进行访问时,一种可能出现的顺序是read a、read b、load b、load a。除此之外,Java内存模型还规定了在执行上述8中基本操作时所必须满足如下规则:不允许read和load、store和write之一单独出现,即不允许一个变量从主内存读取了但工作内存不接收,或者是从工作内存发起回写了但主内存不接受的情况出现。不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。不允许一个线程无原因地(没有发生过assign操作)把数据从线程的工作内存同步回主内存中。一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未初始化(load或assign)的变量。换言之,就是对于一个变量实施use、store操作之前,必须先执行过assign和load操作。一个变量在同一时刻只允许一个线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。如果对一个变量执行lock操作,那将会清空工作内存中的此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不许去unlock一个被其他线程锁定住的变量。对一个变量执行unlock操作之前必须先把该变量同步回主内存中(执行store、write操作)。 这八种内存访问操作以及上述规则限定,再加上稍后介绍的对volatile的一些特殊规定,就已经完全确定了Java程序中哪些内存访问操作在并发下是安全的。由于这种定义相当严谨但确实比较繁琐,实践起来比较麻烦,所以后面会介绍这种定义的一个等效判断原则————先行发生原则,用来确定一个访问在并发环境下是否安全。对于volatile型变量的特殊规则 关键字可以说是Java虚拟机提供的最轻量级的同步机制,但它并不是很容易完全被正确、完整地理解。以至于许多程序员都习惯不去使用它,遇到需要处理多线程数据竞争的时候一律使用synchronized来进行同步。 当将一个变量定义为volatile之后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能保证这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行回写,另一条线程B在线程A回写完成了之后在从主内存进行读取操作,新变量的值才会对线程B可见。第二是禁止指令重排序优化,普通的变量仅仅会保证在该方法执行的过程中所有依赖赋值的结果都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知这点,这就是Java内存模型当中所描述的“线程内表现为串行”的语义(Within Thread As-If Serial Semantics)。可见性 关于volatile可见性:volatile变量是对所有线程可见的,对volatile变量所有的写操作都能立刻反映到其他线程之中,换言之,volatile变量在各个线程中是一致的。但一致并不代表基于volatile变量的运算在并发下是安全的。volatile变量在各个线程的工作内存中不存在一致性的问题(在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但在每次使用之前都要先刷新,执行引擎看不到不一致的情况,所以可以认为不存在一致性的问题),但Java里面的运算并非是原子操作,导致volatile;变量在并发下一样是不安全的。比如下例:/** * volatile变量自增测试运算 * @author xpc * @date 2018年12月16日下午8:40:02 /public class VolatileTest { public static volatile int race=0; public static void increase() { race++; } public static final int THREADCOUNT=20; public static void main(String[] args) { Thread[] threads=new Thread[THREADCOUNT]; for(int i=0;i<THREADCOUNT;i++) { threads[i]=new Thread(()->{ for(int j=0;j<10000;j++) increase(); }); threads[i].start(); } while(Thread.activeCount()>1) { Thread.yield(); } System.out.println(race);//最后打印的结果是小于2010000即200000的数 }}其自增部分对应的字节码为 public static void increase(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=0, args_size=0 0: getstatic #13 // Field race:I 3: iconst_1 4: iadd 5: putstatic #13 // Field race:I 8: return LineNumberTable: line 11: 0 line 12: 8 LocalVariableTable: Start Length Slot Name Signature 之所以最后输出的结果小于200000,并且每次运行程序输出的结果都不一样。问题就出现在自增运算race++上,反编译后发现一个race++会产生4条字节码指令(不包括return),从字节码层面很容易分析出并发失败的原因:当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但在执行iconst_1、iadd这些指令的时候,其他线程可能已经把race的值加大了,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存之中。 这里使用字节码来分析并发问题仍然是不严谨的,因为即使编译出来只有一条字节码指令,也不意味这执行这条指令就是一个原子操作。一个字节码指令在解释执行时,解释器将要运行许多行代码才能实现它的语义。如果是编译执行,一条字节码指令可能转化为若干条本地机器码指令。 由于volatile只能保证可见性,在不符合以下两条规则的运算场景中,仍然要通过加锁(使用synchronized或者java.util.concurrent中的原子类)来保证原子性。同时满足以下两条规则的运算场景才适合使用volatile去保证原子性运算结果并不依赖变量的当前值,或者,能够确保只有单一线程修改变量的值变量不需要与其他的状态变量共同参与不变约束满足第一条但不满足第二条的一个例子:volatile static int start = 3;volatile static int end = 6;//只有线程B修改变量的值,满足了第一条。尽管不满足运算结果不依赖变量的当前值,false||ture==ture线程A执行如下代码:while (start < end){//do something}线程B执行如下代码:start+=3;end+=3;适合使用volatile来控制并发的场景的例子,当shutdown()方法被调用时,能保证所有线程中执行的doWork()方法都立即停止下来。volatile boolean shutdownRequested;public void shutdown(){ shutdownRequested=ture;}public void doWork(){ while(!shutdownRequested){ //do stuff }}禁止指令重排序指令重排序干扰程序的并发执行的例子Map configOptions;char[] configText;//此变量必须定义为volatilevolatile boolean initialized=false;//假设以下代码在线程A中执行//模拟读取配置信息,当读取完后将initialized设置为true以通知其他线程配置可用configOptions=new HashMap();configText=readConfigFile(fileName);processConfigOptions(configText,configOptions);initialized=true;//假设以下代码在线程B中执行//等待initialized为true,代表线程A已经把配置信息处理化完成while(!initialized){ sleep();}//使用线程A初始化好的配置信息doSomethingWithConfig(); 在这个例子中,如果定义initialized变量时没有使用volatile修饰,就可能会由于指令重排序的优化,导致位于线程A中的最后一句的代码initialized=true;被提前执行(虽然使用java代码来作为伪代码,但所指的重排序优化是机器级的优化操作,提前执行是值这句话对应的汇编代码被提前执行),这样在线程B中使用配置信息的代码就可能出现错误,而volatile关键字则可以避免此类情况的发生。 ...

December 17, 2018 · 1 min · jiezi

Golang并发模型:轻松入门select

之前的文章都提到过,Golang的并发模型都来自生活,select也不例外。举个例子:我们都知道一句话,“吃饭睡觉打豆豆”,这一句话里包含了3件事:妈妈喊你吃饭,你去吃饭。时间到了,要睡觉。没事做,打豆豆。在Golang里,select就是干这个事的:到吃饭了去吃饭,该睡觉了就睡觉,没事干就打豆豆。结束发散,我们看下select的功能,以及它能做啥。select功能在多个通道上进行读或写操作,让函数可以处理多个事情,但1次只处理1个。以下特性也都必须熟记于心:每次执行select,都会只执行其中1个case或者执行default语句。当没有case或者default可以执行时,select则阻塞,等待直到有1个case可以执行。当有多个case可以执行时,则随机选择1个case执行。case后面跟的必须是读或者写通道的操作,否则编译出错。select长下面这个样子,由select和case组成,default不是必须的,如果没其他事可做,可以省略default。func main() { readCh := make(chan int, 1) writeCh := make(chan int, 1) y := 1 select { case x := <-readCh: fmt.Printf(“Read %d\n”, x) case writeCh <- y: fmt.Printf(“Write %d\n”, y) default: fmt.Println(“Do what you want”) }}我们创建了readCh和writeCh2个通道:readCh中没有数据,所以case x := <-readCh读不到数据,所以这个case不能执行。writeCh是带缓冲区的通道,它里面是空的,可以写入1个数据,所以case writeCh <- y可以执行。有case可以执行,所以default不会执行。这个测试的结果是$ go run example.goWrite 1用打豆豆实践select来,我们看看select怎么实现打豆豆:eat()函数会启动1个协程,该协程先睡几秒,事件不定,然后喊你吃饭,main()函数中的sleep是个定时器,每3秒喊你吃1次饭,select则处理3种情况:从eatCh中读到数据,代表有人喊我吃饭,我要吃饭了。从sleep.C中读到数据,代表闹钟时间到了,我要睡觉。default是,没人喊我吃饭,也不到时间睡觉,我就打豆豆。import ( “fmt” “time” “math/rand”)func eat() chan string { out := make(chan string) go func (){ rand.Seed(time.Now().UnixNano()) time.Sleep(time.Duration(rand.Intn(5)) * time.Second) out <- “Mom call you eating” close(out) }() return out}func main() { eatCh := eat() sleep := time.NewTimer(time.Second * 3) select { case s := <-eatCh: fmt.Println(s) case <- sleep.C: fmt.Println(“Time to sleep”) default: fmt.Println(“Beat DouDou”) }}由于前2个case都要等待一会,所以都不能执行,所以执行default,运行结果一直是打豆豆:$ go run x.goBeat DouDou现在我们不打豆豆了,你把default和下面的打印注释掉,多运行几次,有时候会吃饭,有时候会睡觉,比如这样:$ go run x.goMom call you eating$ go run x.goTime to sleep$ go run x.goTime to sleepselect很简单但功能很强大,它让golang的并发功能变的更强大。这篇文章写的啰嗦了点,重点是为下一篇文章做铺垫,下一篇我们将介绍下select的高级用法。select的应用场景很多,让我总结一下,放在下一篇文章中吧。并发系列文章推荐Golang并发模型:轻松入门流水线模型Golang并发模型:轻松入门流水线FAN模式Golang并发模型:并发协程的优雅退出Golang并发模型:轻松入门select如果这篇文章对你有帮助,请点个赞/喜欢,鼓励我持续分享,感谢。我的文章列表,点此可查看如果喜欢本文,随意转载,但请保留此原文链接。 ...

December 12, 2018 · 1 min · jiezi

Golang并发模型:轻松入门流水线FAN模式

前一篇文章《Golang并发模型:轻松入门流水线模型》,介绍了流水线模型的概念,这篇文章是流水线模型进阶,介绍FAN-IN和FAN-OUT,FAN模式可以让我们的流水线模型更好的利用Golang并发,提高软件性能。但FAN模式不一定是万能,不见得能提高程序的性能,甚至还不如普通的流水线。我们先介绍下FAN模式,再看看它怎么提升性能的,它是不是万能的。这篇文章内容略多,本来打算分几次写的,但不如一次读完爽,所以干脆还是放一篇文章了,要是时间不充足,利用好碎片时间,可以每次看1个标题的内容。FAN-IN和FAN-OUT模式Golang的并发模式灵感来自现实世界,这些模式是通用的,毫无例外,FAN模式也是对当前世界的模仿。以汽车组装为例,汽车生产线上有个阶段是给小汽车装4个轮子,可以把这个阶段任务交给4个人同时去做,这4个人把轮子都装完后,再把汽车移动到生产线下一个阶段。这个过程中,就有任务的分发,和任务结果的收集。其中任务分发是FAN-OUT,任务收集是FAN-IN。FAN-OUT模式:多个goroutine从同一个通道读取数据,直到该通道关闭。OUT是一种张开的模式,所以又被称为扇出,可以用来分发任务。FAN-IN模式:1个goroutine从多个通道读取数据,直到这些通道关闭。IN是一种收敛的模式,所以又被称为扇入,用来收集处理的结果。FAN-IN和FAN-OUT实践我们这次试用FAN-OUT和FAN-IN,解决《Golang并发模型:轻松入门流水线模型》中提到的问题:计算一个整数切片中元素的平方值并把它打印出来。producer()保持不变,负责生产数据。squre()也不变,负责计算平方值。修改main(),启动3个square,这3个squre从producer生成的通道读数据,这是FAN-OUT。增加merge(),入参是3个square各自写数据的通道,给这3个通道分别启动1个协程,把数据写入到自己创建的通道,并返回该通道,这是FAN-IN。FAN模式流水线示例:package mainimport ( “fmt” “sync”)func producer(nums …int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { out <- i } }() return out}func square(inCh <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range inCh { out <- n * n } }() return out}func merge(cs …<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup collect := func(in <-chan int) { defer wg.Done() for n := range in { out <- n } } wg.Add(len(cs)) // FAN-IN for _, c := range cs { go collect(c) } // 错误方式:直接等待是bug,死锁,因为merge写了out,main却没有读 // wg.Wait() // close(out) // 正确方式 go func() { wg.Wait() close(out) }() return out}func main() { in := producer(1, 2, 3, 4) // FAN-OUT c1 := square(in) c2 := square(in) c3 := square(in) // consumer for ret := range merge(c1, c2, c3) { fmt.Printf("%3d “, ret) } fmt.Println()}3个squre协程并发运行,结果顺序是无法确定的,所以你得到的结果,不一定与下面的相同。➜ awesome git:(master) ✗ go run hi.go 1 4 16 9 FAN模式真能提升性能吗?相信你心里已经有了答案,可以的。我们还是使用老问题,对比一下简单的流水线和FAN模式的流水线,修改下代码,增加程序的执行时间:produer()使用参数生成指定数量的数据。square()增加阻塞操作,睡眠1s,模拟阶段的运行时间。main()关闭对结果数据的打印,降低结果处理时的IO对FAN模式的对比。普通流水线:// hi_simple.gopackage mainimport ( “fmt”)func producer(n int) <-chan int { out := make(chan int) go func() { defer close(out) for i := 0; i < n; i++ { out <- i } }() return out}func square(inCh <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range inCh { out <- n * n // simulate time.Sleep(time.Second) } }() return out}func main() { in := producer(10) ch := square(in) // consumer for _ = range ch { }}使用FAN模式的流水线:// hi_fan.gopackage mainimport ( “sync” “time”)func producer(n int) <-chan int { out := make(chan int) go func() { defer close(out) for i := 0; i < n; i++ { out <- i } }() return out}func square(inCh <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range inCh { out <- n * n // simulate time.Sleep(time.Second) } }() return out}func merge(cs …<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup collect := func(in <-chan int) { defer wg.Done() for n := range in { out <- n } } wg.Add(len(cs)) // FAN-IN for _, c := range cs { go collect(c) } // 错误方式:直接等待是bug,死锁,因为merge写了out,main却没有读 // wg.Wait() // close(out) // 正确方式 go func() { wg.Wait() close(out) }() return out}func main() { in := producer(10) // FAN-OUT c1 := square(in) c2 := square(in) c3 := square(in) // consumer for _ = range merge(c1, c2, c3) { }}多次测试,每次结果近似,结果如下:FAN模式利用了7%的CPU,而普通流水线CPU只使用了3%,FAN模式能够更好的利用CPU,提供更好的并发,提高Golang程序的并发性能。FAN模式耗时10s,普通流水线耗时4s。在协程比较费时时,FAN模式可以减少程序运行时间,同样的时间,可以处理更多的数据。➜ awesome git:(master) ✗ time go run hi_simple.gogo run hi_simple.go 0.17s user 0.18s system 3% cpu 10.389 total➜ awesome git:(master) ✗ ➜ awesome git:(master) ✗ time go run hi_fan.gogo run hi_fan.go 0.17s user 0.16s system 7% cpu 4.288 total也可以使用Benchmark进行测试,看2个类型的执行时间,结论相同。为了节约篇幅,这里不再介绍,方法和结果贴在Gist了,想看的朋友瞄一眼,或自己动手搞搞。FAN模式一定能提升性能吗?FAN模式可以提高并发的性能,那我们是不是可以都使用FAN模式?不行的,因为FAN模式不一定能提升性能。依然使用之前的问题,再次修改下代码,其他不变:squre()去掉耗时。main()增加producer()的入参,让producer生产10,000,000个数据。简单版流水线修改代码:// hi_simple.gofunc square(inCh <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range inCh { out <- n * n } }() return out}func main() { in := producer(10000000) ch := square(in) // consumer for _ = range ch { }}FAN模式流水线修改代码:// hi_fan.gopackage mainimport ( “sync”)func square(inCh <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range inCh { out <- n * n } }() return out}func main() { in := producer(10000000) // FAN-OUT c1 := square(in) c2 := square(in) c3 := square(in) // consumer for _ = range merge(c1, c2, c3) { }}结果,可以跑多次,结果近似:➜ awesome git:(master) ✗ time go run hi_simple.go go run hi_simple.go 9.96s user 5.93s system 168% cpu 9.424 total➜ awesome git:(master) ✗ time go run hi_fan.go go run hi_fan.go 23.35s user 11.51s system 297% cpu 11.737 total从这个结果,我们能看到2点。FAN模式可以提高CPU利用率。FAN模式不一定能提升效率,降低程序运行时间。优化FAN模式既然FAN模式不一定能提高性能,如何优化?不同的场景优化不同,要依具体的情况,解决程序的瓶颈。我们当前程序的瓶颈在FAN-IN,squre函数很快就完成,merge函数它把3个数据写入到1个通道的时候出现了瓶颈,适当使用带缓冲通道可以提高程序性能,再修改下代码merge()中的out修改为:out := make(chan int, 100)结果:➜ awesome git:(master) ✗ time go run hi_fan_buffered.go go run hi_fan_buffered.go 19.85s user 8.19s system 323% cpu 8.658 total使用带缓存通道后,程序的性能有了较大提升,CPU利用率提高到323%,提升了8%,运行时间从11.7降低到8.6,降低了26%。FAN模式的特点很简单,相信你已经掌握了,如果记不清了看这里,本文所有代码在该Github仓库。FAN模式很有意思,并且能提高Golang并发的性能,如果想以后运用自如,用到自己的项目中去,还是要写写自己的Demo,快去实践一把。下一篇,写流水线中协程的“优雅退出”,欢迎关注。如果这篇文章对你有帮助,请点个赞/喜欢,让我知道我的写作是有价值的,感谢。 ...

November 28, 2018 · 3 min · jiezi