Java|Java 浅谈 高并发 处理方案详解

目录

  • 高性能开发十大必须掌握的核心技术
    • I/O优化:零拷贝技术
    • I/O优化:多路复用技术
    • 线程池技术
    • 无锁编程技术
    • 进程间通信技术
  • Scale-out(横向拓展)
    • 缓存
      • 异步
        • 高性能、高可用、高拓展 解决方案
          • 高性能的实践方案
          • 高可用的实践方案
          • 高扩展的实践方案
        • 总结

          高性能开发十大必须掌握的核心技术 我们循序渐进,从内存、磁盘I/O、网络I/O、CPU、缓存、架构、算法等多层次递进,串联起高性能开发十大必须掌握的核心技术。
          - I/O优化:零拷贝技术- I/O优化:多路复用技术- 线程池技术- 无锁编程技术- 进程间通信技术- RPC && 序列化技术- 数据库索引技术- 缓存技术 && 布隆过滤器- 全文搜索技术- 负载均衡技术


          I/O优化:零拷贝技术
          从磁盘读文件、再通过网络发送数据,数据从磁盘到网络,兜兜转转需要拷贝四次,其中CPU亲自搬运都需要两次。
          最近在学习nginx的底层设计,正好有看到这个。后面可以在nginx系列里面补上这个。
          Java|Java 浅谈 高并发 处理方案详解
          文章图片

          零拷贝技术,解放CPU,文件数据直接从内核发送出去,无需再拷贝到应用程序缓冲区,白白浪费资源。

          Java|Java 浅谈 高并发 处理方案详解
          文章图片

          Linux API:
          ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

          函数名字已经把函数的功能解释的很明显了:发送文件。指定要发送的文件描述符和网络套接字描述符,一个函数搞定!

          I/O优化:多路复用技术
          每个线程都要阻塞在recv等待对方的请求,这来访问的人多了,线程开的就多了,大量线程都在阻塞,系统运转速度也随之下降。
          这个时候,你需要多路复用技术,使用select模型,将所有等待(accept、recv)都放在主线程里,工作线程不需要再等待。

          Java|Java 浅谈 高并发 处理方案详解
          文章图片

          过了一段时间之后,网站访问的人越来越多了,就连select也开始有点应接不暇,老板继续让你优化性能。
          这个时候,你需要升级多路复用模型为epoll。
          select有三弊,epoll有三优。
          select底层采用数组来管理套接字描述符,同时管理的数量有上限,一般不超过几千个,epoll使用树和链表来管理,同时管理数量可以很大。select不会告诉你到底哪个套接字来了消息,你需要一个个去询问。epoll直接告诉你谁来了消息,不用轮询。select进行系统调用时还需要把套接字列表在用户空间和内核空间来回拷贝,循环中调用select时简直浪费。epoll统一在内核管理套接字描述符,无需来回拷贝。

          用上了epoll多路复用技术,开发了3.0版本,你的网站能同时处理很多用户请求了。
          之前的方案中,工作线程总是用到才创建,用完就关闭,大量请求来的时候,线程不断创建、关闭、创建、关闭,开销挺大的。这个时候,你需要:

          线程池技术
          我们可以在程序一开始启动后就批量启动一波工作线程,而不是在有请求来的时候才去创建,使用一个公共的任务队列,请求来临时,向队列中投递任务,各个工作线程统一从队列中不断取出任务来处理,这就是线程池技术。
          Java|Java 浅谈 高并发 处理方案详解
          文章图片

          多线程技术的使用一定程度提升了服务器的并发能力,但同时,多个线程之间为了数据同步,常常需要使用互斥体、信号、条件变量等手段来同步多个线程。这些重量级的同步手段往往会导致线程在用户态/内核态多次切换,系统调用,线程切换都是不小的开销。
          在线程池技术中,提到了一个公共的任务队列,各个工作线程需要从中提取任务进行处理,这里就涉及到多个工作线程对这个公共队列的同步操作。
          有没有一些轻量级的方案来实现多线程安全的访问数据呢?这个时候,你需要:

          无锁编程技术
          多线程并发编程中,遇到公共数据时就需要进行线程同步。而这里的同步又可以分为阻塞型同步和非阻塞型同步。
          阻塞型同步好理解,我们常用的互斥体、信号、条件变量等这些操作系统提供的机制都属于阻塞型同步,其本质都是要加“锁”。
          Java|Java 浅谈 高并发 处理方案详解
          文章图片

          与之对应的非阻塞型同步就是在无锁的情况下实现同步,目前有三类技术方案:
          Wait-freeLock-freeObstruction-free

          三类技术方案都是通过一定的算法和技术手段来实现不用阻塞等待而实现同步,这其中又以Lock-free最为应用广泛。
          Lock-free能够广泛应用得益于目前主流的CPU都提供了原子级别的read-modify-write原语,这就是著名的CAS(Compare-And-Swap)操作。在Intel x86系列处理器上,就是cmpxchg系列指令。
          我们常常见到的无锁队列、无锁链表、无锁HashMap等数据结构,其无锁的核心大都来源于此。在日常开发中,恰当的运用无锁化编程技术,可以有效地降低多线程阻塞和切换带来的额外开销,提升性能。
          服务器上线了一段时间,发现服务经常崩溃异常,排查发现是工作线程代码bug,一崩溃整个服务都不可用了。于是你决定把工作线程和主线程拆开到不同的进程中,工作线程崩溃不能影响整体的服务。这个时候出现了多进程,你需要:

          进程间通信技术
          提起进程间通信,你能想到的是什么?
          管道命名管道socket消息队列信号信号量共享内存

          以上各种进程间通信的方式详细介绍和比较,推荐一篇文章再探进程间通信,这里不再赘述。

          Scale-out(横向拓展) 采用分布式部署的方式把流量分开,让每个服务器都承担一部分并发和流量。这也是我最喜欢的一种方法。

          缓存 使用缓存来提高系统的性能。
          为什么缓存可以大幅度提升系统的性能呢?
          那肯定是要更普通磁盘进行对比的啊。我们来看看普通磁盘的速度:
          普通磁盘的寻道时间是 10ms 左右,而相比于磁盘寻道花费的时间,CPU 执行指令和内存寻址的时间都在是 ns(纳秒)级别,从千兆网卡上读取数据的时间是在μs(微秒)级别。所以在整个计算机体系中,磁盘是最慢的一环,甚至比其它的组件要慢几个数量级。因此,我们通常使用以内存作为存储介质的缓存,以此提升性能。
          至于缓存为什么快,因为它是内置的啊,在内存中。不过也有个缺点,就是烧内存。

          异步 Java|Java 浅谈 高并发 处理方案详解
          文章图片

          这是业务层面的异步。
          内核层面的异步,需要调用内核指定的异步函数(aio族),不然,不论阻塞还是非阻塞都是同步。

          高性能、高可用、高拓展 解决方案 以下实践方案,有些我已经试过了,有些还没体验但是知道那么一回事儿,有些则不知道啥时候能实践了。
          【Java|Java 浅谈 高并发 处理方案详解】
          高性能的实践方案
          1、集群部署,通过负载均衡减轻单机压力。2、多级缓存,包括静态数据使用CDN、本地缓存、分布式缓存等,以及对缓存场景中的热点key、缓存穿透、缓存并发、数据一致性等问题的处理。3、分库分表和索引优化,以及借助搜索引擎解决复杂查询问题。4、考虑NoSQL数据库的使用,比如HBase、TiDB等,但是团队必须熟悉这些组件,且有较强的运维能力。5、异步化,将次要流程通过多线程、MQ、甚至延时任务进行异步处理。6、限流,需要先考虑业务是否允许限流(比如秒杀场景是允许的),包括前端限流、Nginx接入层的限流、服务端的限流。7、对流量进行削峰填谷,通过MQ承接流量。8、并发处理,通过多线程将串行逻辑并行化。9、预计算,比如抢红包场景,可以提前计算好红包金额缓存起来,发红包时直接使用即可。10、缓存预热,通过异步任务提前预热数据到本地缓存或者分布式缓存中。11、减少IO次数,比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。12、减少IO时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。13、程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化,或者采用更高效的算法。14、各种池化技术的使用和池大小的设置,包括HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。15、JVM优化,包括新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。16、锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。

          上述方案无外乎从计算和 IO 两个维度考虑所有可能的优化点,需要有配套的监控系统实时了解当前的性能表现,并支撑你进行性能瓶颈分析,然后再遵循二八原则,抓主要矛盾进行优化。

          高可用的实践方案
          1、对等节点的故障转移,Nginx和服务治理框架均支持一个节点失败后访问另一个节点。2、非对等节点的故障转移,通过心跳检测并实施主备切换(比如redis的哨兵模式或者集群模式、MySQL的主从切换等)。3、接口层面的超时设置、重试策略和幂等设计。4、降级处理:保证核心服务,牺牲非核心服务,必要时进行熔断;或者核心链路出问题时,有备选链路。5、限流处理:对超过系统处理能力的请求直接拒绝或者返回错误码。6、MQ场景的消息可靠性保证,包括producer端的重试机制、broker侧的持久化、consumer端的ack机制等。7、灰度发布,能支持按机器维度进行小流量部署,观察系统日志和业务指标,等运行平稳后再推全量。8、监控报警:全方位的监控体系,包括最基础的CPU、内存、磁盘、网络的监控,以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。9、灾备演练:类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。

          高可用的方案主要从冗余、取舍、系统运维3个方向考虑,同时需要有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。

          高扩展的实践方案
          1、合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。2、存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。3、业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心接口和非核心接口拆,还可以按照请求源拆(比如To C和To B,APP和H5)。


          总结 1、最简单的系统设计满足业务需求和流量现状,选择最熟悉的技术体系。
          2、随着流量的增加和业务的变化,修正架构中存在问题的点,如单点问题,横向扩展问题,性能无法满足需求的组件。
          在这个过程中,选择社区成熟的、团队熟悉的组件帮助我们解决问题,在社区没有合适解决方案的前提下才会自己造轮子。
          3、当对架构的小修小补无法满足需求时,考虑重构、重写等大的调整方式以解决现有的问题。
          本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!

            推荐阅读