精华内容
下载资源
问答
  • 使用多个服务器并行部署的有用工具。 这个怎么运作 该gem将帮助您同时在多个服务器上部署应用程序。 它使用原始的mina deploy.rb文件,更改application_name , domain并开始部署过程。 安装 将此行添加到您的...
  • 服务器怎么配置才能更流畅稳定的使用 1.使用内存数据库 内存数据库,其实就是将数据放在内存中直接操作的数据库。相对于磁盘,内存的数据读写速度要高出几数量级,将数据保存在内存中相比从磁盘上访问能够极大地...

    服务器怎么配置才能更流畅稳定的使用
    1.使用内存数据库
    内存数据库,其实就是将数据放在内存中直接操作的数据库。相对于磁盘,内存的数据读写速度要高出几个数量级,将数据保存在内存中相比从磁盘上访问能够极大地提高应用的性能。
    内存数据库抛弃了磁盘数据管理的传统方式,基于全部数据都在内存中重新设计了体系结构,并且在数据缓存、快速算法、并行操作方面也进行了相应的改进,所以数据处理速度比传统数据库的数据处理速度要快很多。
    但是安全性的问题可以说是内存数据库最大的硬伤。因为内存本身有掉电丢失的天然缺陷,因此我们在使用内存数据库的时候,通常需要,提前对内存上的数据采取一些保护机制,比如备份,记录日志,热备或集群,与磁盘数据库同步等方式。
    对于一些重要性不高但是又想要快速响应用户请求的部分数据可以考虑内存数据库来存储,同时可以定期把数据固化到磁盘。
    2.使用RDD
    在大数据云计算相关领域的一些应用中,Spark可以用来加快数据处理速度。Spark的核心是RDD,RDD最早来源与Berkeley实验室的一篇论文《Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing》。
    现有的数据流系统对两种应用的处理并不高效:一是迭代式算法,这在图应用和机器学习领域很常见;二是交互式数据挖掘工具。这两种情况下,将数据保存在内存中能够极大地提高性能。
    3.增加缓存
    很多web应用是有大量的静态内容,这些静态内容主要都是一些小文件,并且会被频繁的读,采用Apache以及nginx作为web服务器。
    在web访问量不大的时候,这两个http服务器可以说是非常的迅速和高效,如果负载量很大的时候,我们可以采用在前端搭建cache服务器,将服务器中的静态资源文件缓存到操作系统内存中直接进行读操作,因为直接从内存读取数据的速度要远大于从硬盘读取。
    这个其实也是增加内存的成本来降低访问磁盘带来的时间消耗。
    4.使用SSD
    除了对内存方面的优化,还可以对磁盘这边进行优化。跟传统机械硬盘相比,固态硬盘具有快速读写、质量轻、能耗低以及体积小等特点。
    但是ssd的价格相比传统机械硬盘要贵,有条件的可以使用ssd来代替机械硬盘。
    5.优化数据库
    大部分的服务器请求最终都是要落到数据库中,随着数据量的增加,数据库的访问速度也会越来越慢。想要提升请求处理速度,必须要对原来的单表进行动刀了。
    目前主流的Linux服务器使用的数据库要属mysql了,如果我们使用mysql存储的数据单个表的记录达到千万级别的话,查询速度会很慢的。根据业务上合适的规则对数据库进行分区分表,可以有效提高数据库的访问速度,提升服务器的整体性能。
    另外对于业务上查询请求,在建表的时候可以根据相关需求设置索引等,以提高查询速度。

    展开全文
  • 我是采用线程的,我用了mpich这个并行程序,我原来的服务器是24核48线程,我就设成48,现在这是两CPU,一是24核48线程,一共就是48核96线程,但我设成96之后,CPU还是只有50%的利用率,显示只有48线程在...
  • 当在读这篇文章的时候,你有没有想过,服务器怎么把这篇文章发送给你的呢?...多进程历史上最早出现也是最简单的一种并行处理多个请求的方法就是利用多进程。比如在Linux世界中,我们可以使用fo...

    当在读这篇文章的时候,你有没有想过,服务器是怎么把这篇文章发送给你的呢?

    说简单也简单,不就是一个用户请求吗?服务器根据请求从数据库中捞出这篇文章,然后通过网络发回去。

    说复杂也复杂,服务器是如何并行处理成千上万个用户请求呢?这里面涉及到哪些技术呢?

    这篇文章就来为你解答这个问题。

    多进程

    历史上最早出现也是最简单的一种并行处理多个请求的方法就是利用多进程比如在Linux世界中,我们可以使用fork、exec等系统调用创建多个进程,我们可以在父进程中接收用户的连接请求,然后创建子进程去处理用户请求,就像这样:

    b2351e812eccc3ca5f0c80c5f42e62ce.png

    这种方法的优点就在于:
    1. 编程简单,非常容易理解
    2. 由于各个进程的地址空间是相互隔离的,因此一个进程崩溃后并不会影响其它进程
    3. 充分利用多核资源
    多进程并行处理的优点很明显,但是缺点同样明显:
    1. 各个进程地址空间相互隔离,这一优点也会变成缺点,那就是进程间要想通信就会变得比较困难,你需要借助进程间通信(IPC,interprocess communications)机制,想一想你现在知道哪些进程间通信机制,然后让你用代码实现呢?显然,进程间通信编程相对复杂,而且性能也是一大问题
    2. 我们知道创建进程开销是比线程要大的,频繁的创建销毁进程无疑会加重系统负担。
    幸好,除了进程,我们还有线程。

    多线程

    不是创建进程开销大吗?不是进程间通信困难吗?这些对于线程来说统统不是问题。什么?你还不了解线程,赶紧看看这篇《看完这篇还不懂线程与线程池你来打我》,这里详细讲解了线程这个概念是怎么来的。由于线程共享进程地址空间,因此线程间通信天然不需要借助任何通信机制,直接读取内存就好了。线程创建销毁的开销也变小了,要知道线程就像寄居蟹一样,房子(地址空间)都是进程的,自己只是一个租客,因此非常的轻量级,创建销毁的开销也非常小。9dd1fb25e715659b6bc75d2f20fcd2b2.png我们可以为每个请求创建一个线程,即使一个线程因执行I/O操作——比如读取数据库等——被阻塞暂停运行也不会影响到其它线程,就像这样:a16d95ba7919a0afcedf853b33d34237.png但线程就是完美的、包治百病的吗,显然,计算机世界从来没有那么简单。由于线程共享进程地址空间,这在为线程间通信带来便利的同时也带来了无尽的麻烦。正是由于线程间共享地址空间,因此一个线程崩溃会导致整个进程崩溃退出,同时线程间通信简直太简单了,简单到线程间通信只需要直接读取内存就可以了,也简单到出现问题也极其容易,死锁、线程间的同步互斥、等等,这些极容易产生bug,无数程序员宝贵的时间就有相当一部分用来解决多线程带来的无尽问题虽然线程也有缺点,但是相比多进程来说,线程更有优势,但想单纯的利用多线程就能解决高并发问题也是不切实际的因为虽然线程创建开销相比进程小,但依然也是有开销的,对于动辄数万数十万的链接的高并发服务器来说,创建数万个线程会有性能问题,这包括内存占用、线程间切换,也就是调度的开销。因此,我们需要进一步思考。

    Event Loop:事件驱动

    到目前为止,我们提到“并行”二字就会想到进程、线程。但是,并行编程只能依赖这两项技术吗,并不是这样的。还有另一项并行技术广泛应用在GUI编程以及服务器编程中,这就是近几年非常流行的事件驱动编程,event-based concurrency。大家不要觉得这是一项很难懂的技术,实际上事件驱动编程原理上非常简单。这一技术需要两种原料:
    1. event
    2. 处理event的函数,这一函数通常被称为event handler
    剩下的就简单了:你只需要安静的等待event到来就好,当event到来之后,检查一下event的类型,并根据该类型找到对应的event处理函数,也就是event handler,然后直接调用该event handler就好了。516edbf45027befb15f5adc96fa4a8d4.pngThat's it !以上就是事件驱动编程的全部内容,是不是很简单!从上面的讨论可以看到,我们需要不断的接收event然后处理event,因此我们需要一个循环(用while或者for循环都可以),这个循环被称为Event loop。使用伪代码表示就是这样:
    while(true) {    event = getEvent();    handler(event);}
    Event loop中要做的事情其实是非常简单的,只需要等待event的带来,然后调用相应的event处理函数即可。注意,这段代码只需要运行在一个线程或者进程中,只需要这一个event loop就可以同时处理多个用户请求。有的同学可以依然不明白为什么这样一个event loop可以同时处理多个请求呢?原因很简单,对于web服务器来说,处理一个用户请求时大部分时间其实都用在了I/O操作上,像数据库读写、文件读写、网络读写等。当一个请求到来,简单处理之后可能就需要查询数据库等I/O操作,我们知道I/O是非常慢的,当发起I/O后我们大可以不用等待该I/O操作完成就可以继续处理接下来的用户请求d8492183d986fd2354d1af462fddf8f7.png现在你应该明白了吧,虽然上一个用户请求还没有处理完我们其实就可以处理下一个用户请求了,这也是并行,这种并行就可以用事件驱动编程来处理。这就好比餐厅服务员一样,一个服务员不可能一直等上一个顾客下单、上菜、吃饭、买单之后才接待下一个顾客,服务员是怎么做的呢?当一个顾客下完单后直接处理下一个顾客,当顾客吃完饭后会自己回来买单结账的。看到了吧,同样是一个服务员也可以同时处理多个顾客,这个服务员就相当于这里的Event loop,即使这个event loop只运行在一个线程(进程)中也可以同时处理多个用户请求。相信你已经对事件驱动编程有一个清晰的认知了,那么接下来的问题就是事件驱动、事件驱动,那么这个事件也就是event该怎么获取呢?

    事件来源:IO多路复用

    在《终于明白了,一文彻底理解I/O多路复用》这篇文章中我们知道,在Linux/Unix世界中一切皆文件,而我们的程序都是通过文件描述符来进行I/O操作的,当然对于socket也不例外,那我们该如何同时处理多个文件描述符呢?IO多路复用技术正是用来解决这一问题的,通过IO多路复用技术,我们一次可以监控多个文件描述,当某个文件(socket)可读或者可写的时候我们就能得到通知啦。这样IO多路复用技术就成了event loop的原材料供应商,源源不断的给我们提供各种event,这样关于event来源的问题就解决了。2370ae2d0b689224da85f42721f9a9cb.png当然关于IO多路复用技术的详细讲解请参见《终于明白了,一文彻底理解I/O多路复用》。至此,关于利用事件驱动来实现并发编程的所有问题都解决了吗?event的来源问题解决了,当得到event后调用相应的handler,看上去大功告成了。想一想还有没有其它问题?

    问题:阻塞式IO

    现在,我们可以使用一个线程(进程)就能基于事件驱动进行并行编程,再也没有了多线程中让人恼火的各种锁、同步互斥、死锁等问题了。但是,计算机科学中从来没有出现过一种能解决所有问题的技术,现在没有,在可预期的将来也不会有。那上述方法有什么问题吗?不要忘了,我们event loop是运行在一个线程(进程),这虽然解决了多线程问题,但是如果在处理某个event时需要进行IO操作会怎么样呢?在《读取文件时,程序经历了什么》一文中,我们讲解了最常用的文件读取在底层是如何实现的,程序员最常用的这种IO方式被称为阻塞式IO,也就是说,当我们进行IO操作,比如读取文件时,如果文件没有读取完成,那么我们的程序(线程)会被阻塞而暂停执行,这在多线程中不是问题,因为操作系统还可以调度其它线程。但是在单线程的event loop中是有问题的,原因就在于当我们在event loop中执行阻塞式IO操作时整个线程(event loop)会被暂停运行,这时操作系统将没有其它线程可以调度,因为系统中只有一个event loop在处理用户请求,这样当event loop线程被阻塞暂停运行时所有用户请求都没有办法被处理,你能想象当服务器在处理其它用户请求读取数据库导致你的请求被暂停吗?4a726e008f299081c8ee13faeade9913.png因此,在基于事件驱动编程时有一条注意事项,那就是不允许发起阻塞式IO有的同学可能会问,如果不能发起阻塞式IO的话,那么该怎样进行IO操作呢?有阻塞式IO,就有非阻塞式IO。

    非阻塞IO

    为克服阻塞式IO所带来的问题,现代操作系统开始提供一种新的发起IO请求的方法,这种方法就是异步IO,对应的,阻塞式IO就是同步IO,关于同步和异步这两个概念可以参考《从小白到高手,你需要理解同步与异步》。异步IO时,假设调用aio_read函数(具体的异步IO API请参考具体的操作系统平台),也就是异步读取,当我们调用该函数后可以立即返回,并继续其它事情,虽然此时该文件可能还没有被读取,这样就不会阻塞调用线程了。此外,操作系统还会提供其它方法供调用线程来检测IO操作是否完成。就这样,在操作系统的帮助下IO的阻塞调用问题也解决了。

    基于事件编程的难点

    虽然有异步IO来解决event loop可能被阻塞的问题,但是基于事件编程依然是困难的。首先,我们提到,event loop是运行在一个线程中的,显然一个线程是没有办法充分利用多核资源的,有的同学可能会说那就创建多个event loop实例不就可以了,这样就有多个event loop线程了,但是这样一来多线程问题又会出现。另一点在于编程方面,在《从小白到高手,你需要理解同步与异步》这篇文章中我们讲到过,异步编程需要结合回调函数(关于回调函数请才参考《程序员应如何彻底理解回调函数》),这种编程方式需要把处理逻辑分为两部分,一部分调用方自己处理,另一部分在回调函数中处理,这一编程方式的改变加重了程序员在理解上的负担,基于事件编程的项目后期会很难扩展以及维护。那么有没有更好的方法呢?要找到更好的方法,我们需要解决问题的本质,那么这个本质问题是什么呢?

    更好的方法

    为什么我们要使用异步这种难以理解的方式编程呢?是因为阻塞式编程虽然容易理解但会导致线程被阻塞而暂停运行。那么聪明的你一定会问了,有没有一种方法既能结合同步IO的简单理解又不会因同步调用导致线程被阻塞呢?答案是肯定的,这就是用户态线程,user level thread,也就是大名鼎鼎的协程,关于协程值得单独拿出一篇文章来讲解,就在下一篇。虽然基于事件编程有这样那样的缺点,但是在当今的高性能高并发服务器上基于事件编程方式依然非常流行,但已经不是纯粹的基于单一线程的事件驱动了,而是event loop + multi thread + user level thread。关于这一组合,同样值得拿出一篇文章来讲解,我们将在后续文章中详细讨论。

    总结

    高并发技术从最开始的多进程一路演进到当前的事件驱动,计算机技术就像生物一样也在不断演变进化,但不管怎样,了解历史才能更深刻的理解当下。希望这篇文章能对大家理解高并发服务器有所帮助。70d04b81634bb11025895d9adc35f8b6.png码农的荒岛求生

    往期精选

    看完这篇还不懂线程与线程池你来打我读取文件时,程序经历了什么?终于明白了,一文彻底理解I/O多路复用从小白到高手,你需要理解同步与异步

    程序员应如何彻底理解回调函数

    e3fd7cc1da36895f104c6be5a825a932.png
    展开全文
  • 当在读这篇文章的时候,你有没有想过,服务器怎么把这篇文章发送给你的呢?...多进程历史上最早出现也是最简单的一种并行处理多个请求的方法就是利用多进程。比如在Linux世界中,我们可以使用fo...

    当在读这篇文章的时候,你有没有想过,服务器是怎么把这篇文章发送给你的呢?

    说简单也简单,不就是一个用户请求吗?服务器根据请求从数据库中捞出这篇文章,然后通过网络发回去。

    说复杂也复杂,服务器是如何并行处理成千上万个用户请求呢?这里面涉及到哪些技术呢?

    这篇文章就来为你解答这个问题。

    多进程

    历史上最早出现也是最简单的一种并行处理多个请求的方法就是利用多进程比如在Linux世界中,我们可以使用fork、exec等系统调用创建多个进程,我们可以在父进程中接收用户的连接请求,然后创建子进程去处理用户请求,就像这样:55b858ce74c1bcfc4059b13afa3d9d05.png这种方法的优点就在于:
    1. 编程简单,非常容易理解
    2. 由于各个进程的地址空间是相互隔离的,因此一个进程崩溃后并不会影响其它进程
    3. 充分利用多核资源
    多进程并行处理的优点很明显,但是缺点同样明显:
    1. 各个进程地址空间相互隔离,这一优点也会变成缺点,那就是进程间要想通信就会变得比较困难,你需要借助进程间通信(IPC,interprocess communications)机制,想一想你现在知道哪些进程间通信机制,然后让你用代码实现呢?显然,进程间通信编程相对复杂,而且性能也是一大问题
    2. 我们知道创建进程开销是比线程要大的,频繁的创建销毁进程无疑会加重系统负担。
    幸好,除了进程,我们还有线程。 多线程不是创建进程开销大吗?不是进程间通信困难吗?这些对于线程来说统统不是问题。什么?你还不了解线程,赶紧看看这篇《看完这篇还不懂线程与线程池你来打我》,这里详细讲解了线程这个概念是怎么来的。由于线程共享进程地址空间,因此线程间通信天然不需要借助任何通信机制,直接读取内存就好了。线程创建销毁的开销也变小了,要知道线程就像寄居蟹一样,房子(地址空间)都是进程的,自己只是一个租客,因此非常的轻量级,创建销毁的开销也非常小。我们可以为每个请求创建一个线程,即使一个线程因执行I/O操作——比如读取数据库等——被阻塞暂停运行也不会影响到其它线程,就像这样:028ddb95ea111d70e17e928a51d589bb.png但线程就是完美的、包治百病的吗,显然,计算机世界从来没有那么简单。由于线程共享进程地址空间,这在为线程间通信带来便利的同时也带来了无尽的麻烦。正是由于线程间共享地址空间,因此一个线程崩溃会导致整个进程崩溃退出,同时线程间通信简直太简单了,简单到线程间通信只需要直接读取内存就可以了,也简单到出现问题也极其容易,死锁、线程间的同步互斥、等等,这些极容易产生bug,无数程序员宝贵的时间就有相当一部分用来解决多线程带来的无尽问题虽然线程也有缺点,但是相比多进程来说,线程更有优势,但想单纯的利用多线程就能解决高并发问题也是不切实际的因为虽然线程创建开销相比进程小,但依然也是有开销的,对于动辄数万数十万的链接的高并发服务器来说,创建数万个线程会有性能问题,这包括内存占用、线程间切换,也就是调度的开销。因此,我们需要进一步思考。 Event Loop:事件驱动到目前为止,我们提到“并行”二字就会想到进程、线程。但是,并行编程只能依赖这两项技术吗,并不是这样的。还有另一项并行技术广泛应用在GUI编程以及服务器编程中,这就是近几年非常流行的事件驱动编程,event-based concurrency。大家不要觉得这是一项很难懂的技术,实际上事件驱动编程原理上非常简单。这一技术需要两种原料:
    1. event
    2. 处理event的函数,这一函数通常被称为event handler
    剩下的就简单了:你只需要安静的等待event到来就好,当event到来之后,检查一下event的类型,并根据该类型找到对应的event处理函数,也就是event handler,然后直接调用该event handler就好了。b2263a3779c97d9bd0dc09dd9bc5ab7b.pngThat's it !以上就是事件驱动编程的全部内容,是不是很简单!从上面的讨论可以看到,我们需要不断的接收event然后处理event,因此我们需要一个循环(用while或者for循环都可以),这个循环被称为Event loop。使用伪代码表示就是这样:
    while(true) {  event = getEvent();      handler(event);}
    Event loop中要做的事情其实是非常简单的,只需要等待event的带来,然后调用相应的event处理函数即可。注意,这段代码只需要运行在一个线程或者进程中,只需要这一个event loop就可以同时处理多个用户请求。有的同学可以依然不明白为什么这样一个event loop可以同时处理多个请求呢?原因很简单,对于web服务器来说,处理一个用户请求时大部分时间其实都用在了I/O操作上,像数据库读写、文件读写、网络读写等。当一个请求到来,简单处理之后可能就需要查询数据库等I/O操作,我们知道I/O是非常慢的,当发起I/O后我们大可以不用等待该I/O操作完成就可以继续处理接下来的用户请求aacc819d2fe7e2088194410ea424a67b.png现在你应该明白了吧,虽然上一个用户请求还没有处理完我们其实就可以处理下一个用户请求了,这也是并行,这种并行就可以用事件驱动编程来处理。这就好比餐厅服务员一样,一个服务员不可能一直等上一个顾客下单、上菜、吃饭、买单之后才接待下一个顾客,服务员是怎么做的呢?当一个顾客下完单后直接处理下一个顾客,当顾客吃完饭后会自己回来买单结账的。看到了吧,同样是一个服务员也可以同时处理多个顾客,这个服务员就相当于这里的Event loop,即使这个event loop只运行在一个线程(进程)中也可以同时处理多个用户请求。相信你已经对事件驱动编程有一个清晰的认知了,那么接下来的问题就是事件驱动、事件驱动,那么这个事件也就是event该怎么获取呢?事件来源:IO多路复用在Linux/Unix世界中一切皆文件,而我们的程序都是通过文件描述符来进行I/O操作的,当然对于socket也不例外,那我们该如何同时处理多个文件描述符呢?IO多路复用技术正是用来解决这一问题的,通过IO多路复用技术,我们一次可以监控多个文件描述,当某个文件(socket)可读或者可写的时候我们就能得到通知啦。这样IO多路复用技术就成了event loop的原材料供应商,源源不断的给我们提供各种event,这样关于event来源的问题就解决了。4beab23fcce603289178ba1c28b48f1e.png至此,关于利用事件驱动来实现并发编程的所有问题都解决了吗?event的来源问题解决了,当得到event后调用相应的handler,看上去大功告成了。想一想还有没有其它问题? 问题:阻塞式IO现在,我们可以使用一个线程(进程)就能基于事件驱动进行并行编程,再也没有了多线程中让人恼火的各种锁、同步互斥、死锁等问题了。但是,计算机科学中从来没有出现过一种能解决所有问题的技术,现在没有,在可预期的将来也不会有。那上述方法有什么问题吗?不要忘了,我们event loop是运行在一个线程(进程),这虽然解决了多线程问题,但是如果在处理某个event时需要进行IO操作会怎么样呢?最常用的文件读取在底层是如何实现的,程序员最常用的这种IO方式被称为阻塞式IO,也就是说,当我们进行IO操作,比如读取文件时,如果文件没有读取完成,那么我们的程序(线程)会被阻塞而暂停执行,这在多线程中不是问题,因为操作系统还可以调度其它线程。但是在单线程的event loop中是有问题的,原因就在于当我们在event loop中执行阻塞式IO操作时整个线程(event loop)会被暂停运行,这时操作系统将没有其它线程可以调度,因为系统中只有一个event loop在处理用户请求,这样当event loop线程被阻塞暂停运行时所有用户请求都没有办法被处理,你能想象当服务器在处理其它用户请求读取数据库导致你的请求被暂停吗?bf4171446af8fb72353263c3fb977f24.png因此,在基于事件驱动编程时有一条注意事项,那就是不允许发起阻塞式IO有的同学可能会问,如果不能发起阻塞式IO的话,那么该怎样进行IO操作呢?有阻塞式IO,就有非阻塞式IO。 非阻塞IO为克服阻塞式IO所带来的问题,现代操作系统开始提供一种新的发起IO请求的方法,这种方法就是异步IO,对应的,阻塞式IO就是同步IO。异步IO时,假设调用aio_read函数(具体的异步IO API请参考具体的操作系统平台),也就是异步读取,当我们调用该函数后可以立即返回,并继续其它事情,虽然此时该文件可能还没有被读取,这样就不会阻塞调用线程了。此外,操作系统还会提供其它方法供调用线程来检测IO操作是否完成。就这样,在操作系统的帮助下IO的阻塞调用问题也解决了。 基于事件编程的难点虽然有异步IO来解决event loop可能被阻塞的问题,但是基于事件编程依然是困难的。首先,我们提到,event loop是运行在一个线程中的,显然一个线程是没有办法充分利用多核资源的,有的同学可能会说那就创建多个event loop实例不就可以了,这样就有多个event loop线程了,但是这样一来多线程问题又会出现。需要把处理逻辑分为两部分,一部分调用方自己处理,另一部分在回调函数中处理,这一编程方式的改变加重了程序员在理解上的负担,基于事件编程的项目后期会很难扩展以及维护。那么有没有更好的方法呢?要找到更好的方法,我们需要解决问题的本质,那么这个本质问题是什么呢 更好的方法为什么我们要使用异步这种难以理解的方式编程呢?是因为阻塞式编程虽然容易理解但会导致线程被阻塞而暂停运行。那么聪明的你一定会问了,有没有一种方法既能结合同步IO的简单理解又不会因同步调用导致线程被阻塞呢?答案是肯定的,这就是用户态线程,user level thread,也就是大名鼎鼎的协程,关于协程值得单独拿出一篇文章来讲解,就在下一篇。虽然基于事件编程有这样那样的缺点,但是在当今的高性能高并发服务器上基于事件编程方式依然非常流行,但已经不是纯粹的基于单一线程的事件驱动了,而是event loop + multi thread + user level thread。关于这一组合,同样值得拿出一篇文章来讲解,我们将在后续文章中详细讨论。 总结高并发技术从最开始的多进程一路演进到当前的事件驱动,计算机技术就像生物一样也在不断演变进化,但不管怎样,了解历史才能更深刻的理解当下。希望这篇文章能对大家理解高并发服务器有所帮助。

    有道无术,术可成;有术无道,止于术

    欢迎大家关注Java之道公众号

    454ea8e090f251aa4889c07a8793fdb9.png

    好文章,我在看❤️

    展开全文
  • 使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使用线程池,是一开发人员必修的基本功。本文...

    随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流。使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使用线程池,是一个开发人员必修的基本功。

    本文开篇简述线程池概念和用途,接着结合线程池的源码,帮助读者领略线程池的设计思路,最后回归实践,通过案例讲述使用线程池遇到的问题,并给出了一种动态化线程池解决方案。

    一、写在前面

    1.1 线程池是什么

    线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。

    线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

    而本文描述线程池是JDK中提供的ThreadPoolExecutor类。

    当然,使用线程池可以带来一系列好处:

    • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
    • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
    • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
    • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

    1.2 线程池解决的问题是什么

    线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:

    1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
    2. 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
    3. 系统无法合理管理内部的资源分布,会降低系统的稳定性。

    为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。

    Pooling is the grouping together of resources (assets, equipment, personnel, effort, etc.) for the purposes of maximizing advantage or minimizing risk to the users. The term is used in finance, computing and equipment management.——wikipedia

    “池化”思想不仅仅能应用在计算机领域,在金融、设备、人员管理、工作管理等领域也有相关的应用。

    在计算机领域中的表现为:统一管理IT资源,包括服务器、存储、和网络资源等等。通过共享资源,使用户在低投入中获益。除去线程池,还有其他比较典型的几种使用策略包括:

    1. 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
    2. 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
    3. 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。

    在了解完“是什么”和“为什么”之后,下面我们来一起深入一下线程池的内部实现原理。

    二、线程池核心设计与实现

    在前文中,我们了解到:线程池是一种通过“池化”思想,帮助我们管理线程而获取并发性的工具,在Java中的体现是ThreadPoolExecutor类。那么它的的详细设计与实现是什么样的呢?我们会在本章进行详细介绍。

    2.1 总体设计

    Java中的线程池核心实现类是ThreadPoolExecutor,本章基于JDK 1.8的源码来分析Java线程池的核心设计与实现。我们首先来看一下ThreadPoolExecutor的UML类图,了解下ThreadPoolExecutor的继承关系。

    b2630222b1fbf645c3eae006a43d0ee5.png
    图1 ThreadPoolExecutor UML类图

    ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增加了一些能力:(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;(2)提供了管控线程池的方法,比如停止线程池的运行。AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

    ThreadPoolExecutor是如何运行,如何同时维护线程和执行任务的呢?其运行机制如下图所示:

    b59febaa5d58ea66dd132d0197ec2c14.png
    图2 ThreadPoolExecutor运行流程

    线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

    接下来,我们会按照以下三个部分去详细讲解线程池运行机制:

    1. 线程池如何维护自身状态。
    2. 线程池如何管理任务。
    3. 线程池如何管理线程。

    2.2 生命周期管理

    线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量 (workerCount)。在具体实现中,线程池将运行状态(runState)、线程数量 (workerCount)两个关键参数的维护放在了一起,如下代码所示:

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

    ctl这个AtomicInteger类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。

    关于内部封装的获取生命周期状态、获取线程池线程数量的计算方法如以下代码所示:

    private static int runStateOf(int c)     { return c & ~CAPACITY; } //计算当前运行状态
    private static int workerCountOf(int c)  { return c & CAPACITY; }  //计算当前线程数量
    private static int ctlOf(int rs, int wc) { return rs | wc; }   //通过状态和线程数生成ctl

    ThreadPoolExecutor的运行状态有5种,分别为:

    459b096d8fa6073c7a87d965c2ffc2fe.png

    其生命周期转换如下入所示:

    131bb48b956324f386a670dd9c9fb34a.png
    图3 线程池生命周期

    2.3 任务执行机制

    2.3.1 任务调度

    任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。

    首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

    1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
    2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
    3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
    4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
    5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

    其执行流程如下图所示:

    b0bcb2dea6d1da1c09722fa78810cd0a.png
    图4 任务调度流程

    2.3.2 任务缓冲

    任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

    阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

    下图中展示了线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素:

    e633b12e334bd2334c9e60e9c0d031bb.png
    图5 阻塞队列

    使用不同的队列可以实现不一样的任务存取策略。在这里,我们可以再介绍下阻塞队列的成员:

    a32b13eebabb48dc4eff40ca6b5ed46c.png

    2.3.3 任务申请

    由上文的任务分配部分可知,任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。

    线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。这部分策略由getTask方法实现,其执行流程如下图所示:

    fa294c71a2b183303e2f1a4e4c22b61a.png
    图6 获取任务流程图

    getTask这部分进行了多次判断,为的是控制线程的数量,使其符合线程池的状态。如果线程池现在不应该持有那么多线程,则会返回null值。工作线程Worker会不断接收新任务去执行,而当工作线程Worker接收不到任务的时候,就会开始被回收。

    2.3.4 任务拒绝

    任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

    拒绝策略是一个接口,其设计如下:

    public interface RejectedExecutionHandler {
        void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
    }

    用户可以通过实现这个接口去定制拒绝策略,也可以选择JDK提供的四种已有拒绝策略,其特点如下:

    40bc99b65cc1571ac85abf36a3a0fc11.png

    2.4 Worker线程管理

    2.4.1 Worker线程

    线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker。我们来看一下它的部分代码:

    private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
        final Thread thread;//Worker持有的线程
        Runnable firstTask;//初始化的任务,可以为null
    }

    Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务;firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。

    Worker执行任务的模型如下图所示:

    42a0ad10a50ea22d84a5a76e5f1aa7d5.png
    图7 Worker执行任务

    线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。

    ​Worker是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。

    1.lock方法一旦获取了独占锁,表示当前线程正在执行任务中。 2.如果正在执行任务,则不应该中断线程。 3.如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。 4.线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。

    在线程回收过程中就使用到了这种特性,回收过程如下图所示:

    eb689296a2cb0fd64c799f41063436ea.png
    图8 线程池回收过程

    2.4.2 Worker线程增加

    增加线程是通过线程池中的addWorker方法,该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。addWorker方法有两个参数:firstTask、core。firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空;core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize,其执行流程如下图所示:

    d7e8a89dc10f20f70d1f13888c810b07.png
    图9 申请线程执行流程图

    2.4.3 Worker线程回收

    线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。

    try {
      while (task != null || (task = getTask()) != null) {
        //执行任务
      }
    } finally {
      processWorkerExit(w, completedAbruptly);//获取不到任务时,主动回收自己
    }

    线程回收的工作是在processWorkerExit方法完成的。

    5bdba0fba2eefe6397eb309f2dc19934.png
    图10 线程销毁流程

    事实上,在这个方法中,将线程引用移出线程池就已经结束了线程销毁的部分。但由于引起线程销毁的可能性有很多,线程池还要判断是什么引发了这次销毁,是否要改变线程池的现阶段状态,是否要根据新状态,重新分配线程。

    2.4.4 Worker线程执行任务

    在Worker类中的run方法调用了runWorker方法来执行任务,runWorker方法的执行过程如下:

    1.while循环不断地通过getTask()方法获取任务。 2.getTask()方法从阻塞队列中取任务。 3.如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。 4.执行任务。 5.如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。

    执行流程如下图所示:

    5fbed7540b5ed430733029a11177cdef.png
    图11 执行任务流程

    三、线程池在业务中的实践

    3.1 业务背景

    在当今的互联网业界,为了最大程度利用CPU的多核性能,并行运算的能力是不可或缺的。通过线程池管理线程获取并发性是一个非常基础的操作,让我们来看两个典型的使用线程池获取并发性的场景。

    场景1:快速响应用户请求

    描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。

    分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。

    4292656c4d1255024b6ab244b259cb48.png
    图12 并行执行任务提升任务响应速度

    场景2:快速处理批量任务

    描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。

    分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

    7716881b223dcc167363cfacbaede61f.png
    图13 并行执行任务提升批量任务执行速度

    3.2 实际问题及方案思考

    线程池使用面临的核心的问题在于:线程池的参数并不好配置。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,这导致业界并没有一些成熟的经验策略帮助开发人员参考。

    关于线程池配置不合理引发的故障,公司内部有较多记录,下面举一些例子:

    Case1:2018年XX页面展示接口大量调用降级:

    事故描述:XX页面展示接口产生大量调用降级,数量级在几十到上百。

    事故原因:该服务展示接口内部逻辑使用线程池做并行计算,由于没有预估好调用的流量,导致最大核心数设置偏小,大量抛出RejectedExecutionException,触发接口降级条件,示意图如下:

    ae17dbcb81c2cbf5ca7af3817936b95f.png
    图14 线程数核心设置过小引发RejectExecutionException

    Case2:2018年XX业务服务不可用S2级故障

    事故描述:XX业务提供的服务执行时间过长,作为上游服务整体超时,大量下游服务调用失败。

    事故原因:该服务处理请求内部逻辑使用线程池做资源隔离,由于队列设置过长,最大线程数设置失效,导致请求数量增加时,大量任务堆积在队列中,任务执行时间过长,最终导致下游服务的大量调用超时失败。示意图如下:

    d4f4d6733fe8cef53d0654869c8d12ff.png
    图15 线程池队列长度设置过长、corePoolSize设置过小导致任务执行速度低

    业务中要使用线程池,而使用不当又会导致故障,那么我们怎样才能更好地使用线程池呢?针对这个问题,我们下面延展几个方向:

    1. 能否不用线程池?

    回到最初的问题,业务使用线程池是为了获取并发性,对于获取并发性,是否可以有什么其他的方案呢替代?我们尝试进行了一些其他方案的调研:

    017771cd755a7649a718fecd42e565e2.png

    综合考虑,这些新的方案都能在某种情况下提升并行任务的性能,然而本次重点解决的问题是如何更简易、更安全地获得的并发性。另外,Actor模型的应用实际上甚少,只在Scala中使用广泛,协程框架在Java中维护的也不成熟。这三者现阶段都不是足够的易用,也并不能解决业务上现阶段的问题。

    2. 追求参数设置合理性?

    有没有一种计算公式,能够让开发同学很简易地计算出某种场景中的线程池应该是什么参数呢?

    带着这样的疑问,我们调研了业界的一些线程池参数配置方案:

    1b272cbd8106713ac5ea4bc94ac4afac.png

    调研了以上业界方案后,我们并没有得出通用的线程池计算方式。并发任务的执行情况和任务类型相关,IO密集型和CPU密集型的任务运行起来的情况差异非常大,但这种占比是较难合理预估的,这导致很难有一个简单有效的通用公式帮我们直接计算出结果。

    3. 线程池参数动态化?

    尽管经过谨慎的评估,仍然不能够保证一次计算出来合适的参数,那么我们是否可以将修改线程池参数的成本降下来,这样至少可以发生故障的时候可以快速调整从而缩短故障恢复的时间呢?基于这个思考,我们是否可以将线程池的参数从代码中迁移到分布式配置中心上,实现线程池参数可动态配置和即时生效,线程池参数动态化前后的参数修改流程对比如下:

    97ed6ca11edc566e3ca98839376a3be3.png
    图16 动态修改线程池参数新旧流程对比

    基于以上三个方向对比,我们可以看出参数动态化方向简单有效。

    3.3 动态化线程池

    3.3.1 整体设计

    动态化线程池的核心设计包括以下三个方面:

    1. 简化线程池配置:线程池构造参数有8个,但是最核心的是3个:corePoolSize、maximumPoolSize,workQueue,它们最大程度地决定了线程池的任务分配和线程分配策略。考虑到在实际应用中我们获取并发性的场景主要是两种:(1)并行执行子任务,提高响应速度。这种情况下,应该使用同步队列,没有什么任务应该被缓存下来,而是应该立即执行。(2)并行执行大批次任务,提升吞吐量。这种情况下,应该使用有界队列,使用队列去缓冲大批量的任务,队列容量必须声明,防止任务无限制堆积。所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求,Less is More。
    2. 参数可动态修改:为了解决参数不好配,修改参数成本高等问题。在Java线程池留有高扩展性的基础上,封装线程池,允许线程池监听同步外部的消息,根据消息进行修改配置。将线程池的配置放置在平台侧,允许开发同学简单的查看、修改线程池配置。
    3. 增加线程池监控:对某事物缺乏状态的观测,就对其改进无从下手。在线程池执行任务的生命周期添加监控能力,帮助开发同学了解线程池状态。

    c49d09e7a6e74345f3fce008ef4b42c7.png
    图17 动态化线程池整体设计

    3.3.2 功能架构

    动态化线程池提供如下功能:

    动态调参:支持线程池参数动态调整、界面化操作;包括修改线程池核心大小、最大核心大小、队列长度等;参数修改后及时生效。 任务监控:支持应用粒度、线程池粒度、任务粒度的Transaction监控;可以看到线程池的任务执行情况、最大任务执行时间、平均任务执行时间、95/99线等。 负载告警:线程池队列任务积压到一定值的时候会通过大象(美团内部通讯工具)告知应用开发负责人;当线程池负载数达到一定阈值的时候会通过大象告知应用开发负责人。 操作监控:创建/修改和删除线程池都会通知到应用的开发负责人。 操作日志:可以查看线程池参数的修改记录,谁在什么时候修改了线程池参数、修改前的参数值是什么。 权限校验:只有应用开发负责人才能够修改应用的线程池参数。

    2d43de9880522e9a44327dda3403a2e3.png
    图18 动态化线程池功能架构

    参数动态化

    JDK原生线程池ThreadPoolExecutor提供了如下几个public的setter方法,如下图所示:

    87911c2ab57ef03e705f37696f1fe2bf.png
    图19 JDK 线程池参数设置接口

    JDK允许线程池使用方通过ThreadPoolExecutor的实例来动态设置线程池的核心策略,以setCorePoolSize为方法例,在运行期线程池使用方调用此方法设置corePoolSize之后,线程池会直接覆盖原来的corePoolSize值,并且基于当前值和原始值的比较结果采取不同的处理策略。对于当前值小于当前工作线程数的情况,说明有多余的worker线程,此时会向当前idle的worker线程发起中断请求以实现回收,多余的worker在下次idel的时候也会被回收;对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务,setCorePoolSize具体流程如下:

    46dbc76ac3b27f716fe7c55c3ff1e872.png
    图20 setCorePoolSize方法执行流程

    线程池内部会处理好当前状态做到平滑修改,其他几个方法限于篇幅,这里不一一介绍。重点是基于这几个public方法,我们只需要维护ThreadPoolExecutor的实例,并且在需要修改的时候拿到实例修改其参数即可。基于以上的思路,我们实现了线程池参数的动态化、线程池参数在管理平台可配置可修改,其效果图如下图所示:

    657011a290f2f8a853ff18e528a3c536.png
    图21 可动态修改线程池参数

    用户可以在管理平台上通过线程池的名字找到指定的线程池,然后对其参数进行修改,保存后会实时生效。目前支持的动态参数包括核心数、最大值、队列长度等。除此之外,在界面中,我们还能看到用户可以配置是否开启告警、队列等待任务告警阈值、活跃度告警等等。关于监控和告警,我们下面一节会对齐进行介绍。

    线程池监控

    除了参数动态化之外,为了更好地使用线程池,我们需要对线程池的运行状况有感知,比如当前线程池的负载是怎么样的?分配的资源够不够用?任务的执行情况是怎么样的?是长任务还是短任务?基于对这些问题的思考,动态化线程池提供了多个维度的监控和告警能力,包括:线程池活跃度、任务的执行Transaction(频率、耗时)、Reject异常、线程池内部统计信息等等,既能帮助用户从多个维度分析线程池的使用情况,又能在出现问题第一时间通知到用户,从而避免故障或加速故障恢复。

    1. 负载监控和告警

    线程池负载关注的核心问题是:基于当前线程池参数分配的资源够不够。对于这个问题,我们可以从事前和事中两个角度来看。事前,线程池定义了“活跃度”这个概念,来让用户在发生Reject异常之前能够感知线程池负载问题,线程池活跃度计算公式为:线程池活跃度 = activeCount/maximumPoolSize。这个公式代表当活跃线程数趋向于maximumPoolSize的时候,代表线程负载趋高。事中,也可以从两方面来看线程池的过载判定条件,一个是发生了Reject异常,一个是队列中有等待任务(支持定制阈值)。以上两种情况发生了都会触发告警,告警信息会通过大象推送给服务所关联的负责人。

    a4af6ca455948c8bcd15c25c48382b0c.png
    图22 大象告警通知

    2. 任务级精细化监控

    在传统的线程池应用场景中,线程池中的任务执行情况对于用户来说是透明的。比如在一个具体的业务场景中,业务开发申请了一个线程池同时用于执行两种任务,一个是发消息任务、一个是发短信任务,这两类任务实际执行的频率和时长对于用户来说没有一个直观的感受,很可能这两类任务不适合共享一个线程池,但是由于用户无法感知,因此也无从优化。动态化线程池内部实现了任务级别的埋点,且允许为不同的业务任务指定具有业务含义的名称,线程池内部基于这个名称做Transaction打点,基于这个功能,用户可以看到线程池内部任务级别的执行情况,且区分业务,任务监控示意图如下图所示:

    a114f3ac64708b1963526e8da2562e83.png
    图23 线程池任务执行监控

    3. 运行时状态实时查看

    用户基于JDK原生线程池ThreadPoolExecutor提供的几个public的getter方法,可以读取到当前线程池的运行状态以及参数,如下图所示:

    e6bebb38db787985de1107f0c6300340.png
    图24 线程池实时运行情况

    动态化线程池基于这几个接口封装了运行时状态实时查看的功能,用户基于这个功能可以了解线程池的实时状态,比如当前有多少个工作线程,执行了多少个任务,队列中等待的任务数等等。效果如下图所示:

    0f5b9ce467822fd701121f050d507117.png
    图25 线程池实时运行情况

    3.4 实践总结

    面对业务中使用线程池遇到的实际问题,我们曾回到支持并发性问题本身来思考有没有取代线程池的方案,也曾尝试着去追求线程池参数设置的合理性,但面对业界方案具体落地的复杂性、可维护性以及真实运行环境的不确定性,我们在前两个方向上可谓“举步维艰”。最终,我们回到线程池参数动态化方向上探索,得出一个且可以解决业务问题的方案,虽然本质上还是没有逃离使用线程池的范畴,但是在成本和收益之间,算是取得了一个很好的平衡。成本在于实现动态化以及监控成本不高,收益在于:在不颠覆原有线程池使用方式的基础之上,从降低线程池参数修改的成本以及多维度监控这两个方面降低了故障发生的概率。希望本文提供的动态化线程池思路能对大家有帮助。

    四、参考资料

    • [1] JDK 1.8源码
    • [2] 维基百科-线程池
    • [3] 更好的使用Java线程池
    • [4] 维基百科Pooling(Resource Management)
    • [5] 深入理解Java线程池:ThreadPoolExecutor
    • [6]《Java并发编程实践》

    作者简介

    • 致远,2018年加入美团点评,美团到店综合研发中心后台开发工程师。
    • 陆晨,2015年加入美团点评,美团到店综合研发中心后台技术专家。

    来源:美团技术团队

    原文:Java线程池实现原理及其在美团业务中的实践

    作者:致远、陆晨

    展开全文
  • 使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。J.U.C提供的线程池ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使用线程池,是一开发人员必修的基本功。本文开篇...
  • 为什么要使用线程?

    2008-11-02 12:05:00
    线程存在的目的就是为了让程序里的多个任务并行地运行。程序里的多个任务可能是多个不同的,比如说在屏幕上绘制的同时响应用户的动作;也可能是多份相同的任务,比如说服务器中的servlet。为了使程序的运行逻辑更加...
  • 前言当您第一次接触 Serverless 的时候,有一不那么明显的新使用方式:与传统的基于服务器的方法相比,Serverless 服务平台可以使您的应用快速水平扩展,并行处理的工作更加有效。这主要是因为 Serverless 可以...
  • 但我们使用配置时,程序最多只能跑20分钟,程序就死掉了。这涉及到tcp的传输,因为跟我们对接的另一端,他们是C语言写的,目前他们没有设置缓冲区大小,应该用的也是默认的,但C程序我不太了解,不知道是多少。...
  • 1.线上的系统用了前端和后端两套工程,并且用nginx做了负载均衡,redis做缓存,而内网版本合并为一工程,直接用node做静态服务器,取消了缓存,这样对于很中小型团队来说很轻便而且也够用了。 2.线上系统在安全...
  • 支持机自动同步 支持断点下载 支持配置自动生成 支持小文件自动合并(减少inode占用) 支持秒传 支持跨域访问 支持一键迁移(搬迁) 支持异地备份(特别是小文件1M以下) 支持并行体验 支持断点续传(tus) 支持docker...
  • EBS环境最简单配置也包括两个服务器,这两个服务器也就是我们熟知的两层:数据库层,和中间层,也叫应用层。数据库层就如字面的意思,就是应用程序的后端数据库。中间层就类似Application Server(应用程序服务器)...
  • 多个浏览器中分别使用 iframe 和 window 方案多次运行同一个测试,以下为 IE6 的两个代表截图: 1. 某次使用 iframe 运行测试的结果 <p><img alt="run in iframe" src=...
  • 并利用具体的例子来全面介绍每特性,不仅讨论了各个特性是什么,还说明了它是如何工作的,如何使用特性来开发软件,以及有关的常见陷阱。  本书面向所有oracle 数据库应用开发人员和dba。 作译者 作者  ...
  • 包括文件、内存结构和进程,锁和闩,事务、并发和版本,表和索引,数据类型,以及分区和并行,并利用具体的例子来充分介绍每特性,不仅讨论了各个特性是什么,还说明了它是如何工作的,如何使用特性来开发...
  • 4.1.8 抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口么。 4.1.9 继承和聚合的区别在哪。 4.2.0 IO模型有哪些,讲讲你理解的nio ,他和bio,aio的区别是啥,谈谈reactor模型...
  • 第4章 使用SQL*Plus和Oracle企业管理器 73 4.1 启动SQL*Plus会话 73 4.1.1 设置环境 73 4.1.2 从命令行启动SQL*Plus会话 74 4.1.3 用CONNECT命令进行连接 75 4.1.4 用/NOLOG的无连接SQL*Plus会话 76 ...
  • 8.8 使用系统函数检测服务器、数据库以及连接级别的配置 246 8.8.1 确定每周的第一天 246 8.8.2 查看当前会话使用的语言 247 8.8.3 查看和设置当前连接锁超时设置 247 8.8.4 显示当前存储过程上下文的嵌套...
  • 17. 数据库组合索引,储存在一个叶子节点还是多个? 44 17.1. 索引的利弊与如何判定,是否需要索引: 44 17.1.1. 索引的好处 44 17.1.2. 索引的弊端 44 17.1.3. 如何判定是否须要创建索引 44 17.2. 复合索引优化 45 ...
  • 第06节、使用线程池支持多个线程同时访问 资源+源码.rar 0009-蚂蚁课堂(每特学院)-2期-NIO编程基础 第01节、IO与NIO区别 第02节、Buffer的数据存取 第03节、make与rest用法 第04节、直接缓冲区与非缓冲区区别 第05...
  • 162.集群中有 3 台服务器,其中一节点宕机,这时候 zookeeper 还可以使用吗? 163.说一下 zookeeper 的通知机制? 十七、MySql 164.数据库的三范式是什么? 165.一张自增表里面总共有 7 条数据,删除了最后 2 条...

空空如也

空空如也

1 2 3
收藏数 58
精华内容 23
关键字:

多个服务器怎么并行使用