● 前言
程序员经常要面临的一个问题就是:如何提高程序性能?
这篇文章,我们循序渐进,从内存、磁盘I/O、网络I/O、CPU、缓存、架构、算法等多层次递进,串联起高性能开发十大必须掌握的核心技术。
主线程进入一个循环,等待连接。
来一个连接就启动一个工作线程来处理。
工作线程中,等待对方请求,然后从磁盘读文件、往套接口发送数据。
● I/O 优化:零拷贝技术
上面的工作线程,从磁盘读文件、再通过网络发送数据,数据从磁盘到网络,兜兜转转需要拷贝四次,其中 CPU 亲自搬运都需要两次。
零拷贝技术,解放 CPU,文件数据直接从内核发送出去,无需再拷贝到应用程序缓冲区,白白浪费资源。
Linux API:
ssize_t sendfile(
int out_fd,
int in_fd,
off_t *offset,
size_t count
);
函数名字已经把函数的功能解释的很明显了:发送文件。指定要发送的文件描述符和网络套接字描述符,一个函数搞定!
用上了零拷贝技术后开发了2.0版本,图片加载速度明显有了提升。不过老板发现同时访问的人变多了以后,又变慢了,又让你继续优化。这个时候,你需要
前面的版本中,每个线程都要阻塞在 recv 等待对方的请求,这来访问的人多了,线程开的就多了,大量线程都在阻塞,系统运转速度也随之下降。
这个时候,你需要多路复用技术,使用 select 模型,将所有等待(accept、recv)都放在主线程里,工作线程不需要再等待。
过了一段时间之后,网站访问的人越来越多了,就连 select 也开始有点应接不暇,老板继续让你优化性能。
这个时候,你需要升级多路复用模型为 epoll。
● select 有三弊,epoll 有三优。
select 底层采用数组来管理套接字描述符,同时管理的数量有上限,一般不超过几千个,epoll 使用树和链表来管理,同时管理数量可以很大。
select 不会告诉你到底哪个套接字来了消息,你需要一个个去询问。epoll 直接告诉你谁来了消息,不用轮询。
select 进行系统调用时还需要把套接字列表在用户空间和内核空间来回拷贝,循环中调用 select 时简直浪费。epoll 统一在内核管理套接字描述符,无需来回拷贝。
用上了 epoll 多路复用技术,开发了3.0版本,你的网站能同时处理很多用户请求了。
创建线程池
我们可以通过自定义ThreadPoolExecutor或者jdk内置的Executors来创建一系列的线程池
newFixedThreadPool: 创建固定线程数量的线程池
newSingleThreadExecutor: 创建单一线程的池
newCachedThreadPool: 创建线程数量自动扩容, 自动销毁的线程池
newScheduledThreadPool: 创建支持计划任务的线程池
潜在宕机风险
使用Executors来创建要注意潜在宕机风险
1.FixedThreadPool和SingleThreadPoolPool : 允许的请求队列长度为 Integer.MAX_VALUE,可能因为无限制任务队列而耗尽资源,只是出现问题的概率较小。如果新请求的到达速率超过了线程池的处理速率,那么新到来的请求将被累积起来可能会堆积大量的请求,从而导致 OOM(内存溢出).
2. CachedThreadPool和ScheduledThreadPool : 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM.
解决方法:这种情况可以采用固定大小的线程池来解决这个问题。
可能有大量请求的线程池场景中, 更推荐自定义ThreadPoolExecutor来创建线程池
线程池大小配置:
一般根据任务类型进行区分, 假设CPU为N核
CPU密集型任务需要减少线程数量, 降低线程切换开销,可配置线程池大小为N + 1.
IO密集型任务则可以加大线程数量, 可配置线程池大小为 N * 2.
混合型任务则可以拆分为CPU密集型与IO密集型, 独立配置.
多线程并发编程中,遇到公共数据时就需要进行线程同步。而这里的同步又可以分为阻塞型同步和非阻塞型同步。
阻塞型同步好理解,我们常用的互斥体、信号、条件变量等这些操作系统提供的机制都属于阻塞型同步,其本质都是要加“锁”。
与之对应的非阻塞型同步就是在无锁的情况下实现同步,目前有三类技术方案:
Wait-free
Lock-free
Obstruction-free
三类技术方案都是通过一定的算法和技术手段来实现不用阻塞等待而实现同步,这其中又以Lock-free最为应用广泛。
Lock-free能够广泛应用得益于目前主流的CPU都提供了原子级别的read-modify-write原语,这就是著名的CAS(Compare-And-Swap)操作。在Intel x86系列处理器上,就是cmpxchg系列指令。
// 通过CAS操作实现Lock-free
do {
...
} while(!CAS(ptr,old_data,new_data ))
我们常常见到的无锁队列、无锁链表、无锁HashMap等数据结构,其无锁的核心大都来源于此。在日常开发中,恰当的运用无锁化编程技术,可以有效地降低多线程阻塞和切换带来的额外开销,提升性能。
管道
命名管道
socket
消息队列
信号
信号量
共享内存
以上各种进程间通信的方式详细介绍和比较,推荐一篇文章一文掌握进程间通信,这里不再赘述。
对于本地进程间需要高频次的大量数据交互,首推共享内存这种方案。
现代操作系统普遍采用了基于虚拟内存的管理方案,在这种内存管理方式之下,各个进程之间进行了强制隔离。程序代码中使用的内存地址均是一个虚拟地址,由操作系统的内存管理算法提前分配映射到对应的物理内存页面,CPU在执行代码指令时,对访问到的内存地址再进行实时的转换翻译。
从上图可以看出,不同进程之中,虽然是同一个内存地址,最终在操作系统和CPU的配合下,实际存储数据的内存页面却是不同的。
而共享内存这种进程间通信方案的核心在于:如果让同一个物理内存页面映射到两个进程地址空间中,双方不是就可以直接读写,而无需拷贝了吗?
当然,共享内存只是最终的数据传输载体,双方要实现通信还得借助信号、信号量等其他通知机制。
用上了高性能的共享内存通信机制,多个服务进程之间就可以愉快的工作了,即便有工作进程出现Crash,整个服务也不至于瘫痪。
满足于只能提供静态网页浏览了,需要能够实现动态交互。这一次老板还算良心,给你加了一台硬件服务器。
于是你用Java/PHP/Python等语言搞了一套web开发框架,单独起了一个服务,用来提供动态网页支持,和原来等静态内容服务器配合工作。
这个时候你发现,静态服务和动态服务之间经常需要通信。
一开始你用基于HTTP的RESTful接口在服务器之间通信,后来发现用JSON格式传输数据效率低下,你需要更高效的通信方案。
这个时候你需要:
RPC && 序列化技术
什么是RPC技术?
RPC全称Remote Procedure Call,远程过程调用。我们平时编程中,随时都在调用函数,这些函数基本上都位于本地,也就是当前进程某一个位置的代码块。但如果要调用的函数不在本地,而在网络上的某个服务器上呢?这就是远程过程调用的来源。
来源:
https://www.toutiao.com/i6946920710614909453/
“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com
IT大咖说 | 关于版权
由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!
感谢您对IT大咖说的热心支持!
相关推荐
推荐文章