前提
如果前端频繁刷新页面或者频繁断开重连将会导致服务端 cpu 升高
解决方法
- 使用 localStorage 缓存连接,每次通信都使用这个连接
代码
let socket;
if(localStorage.getItem('socket')) {
socket = JSON.parse(localStorage.getItem('socket'));
// 创建一个不自动连接的 socket 对象
let temp = io({autoConnect: false});
Object.setPrototypeOf(socket, Object.getPrototypeOf(temp));
delete temp;
} else {
socket = io('http://localhost:5000', {'timeout':5000, 'connect timeout':5000});
localStorage.setItem('socket', JSON.stringify(JSON.decycle(socket)));
}
出现的问题
- localStorage 只能存储字符串,所以使用 stringify 将对象转成字符串,但是转化过程中出现循环引用问题
解决方法:https://juejin.cn/post/6904563374873395214#heading-2
- 现在可以将对象存到缓存中了,但是原型丢失,导致这个对象上的方法无法使用
解决方法:
- 可以将原型也缓存,直到最后 Object,但是 stringify 不可以序列化对象上的函数,所以这种方法放弃
- 可以创建一个
io({autoConnect: false})
对象,表示不自动连接,需要手动调用connect
进行连接,然后将这个对象上的原型赋给从缓存中拿到的那个对象就可以了
解决完毕!睡觉!😎
醒来了!又出 bug 了!
- 主要想解决的问题是socket.io 频繁连接所导致的 CPU 升高问题,其实不仅是 socket.io 频繁连接,如果是 Http 频繁连接,照样会导致 CPU 升高。咱们从客户端和服务器两个角度来分析怎样做,如果是客户端,使用缓存连接的方法,可以在服务端避免大量创建和销毁套接字,这在原理上是行的通的,但是,当刷新页面的时候,这个连接会被销毁,会给服务器发送 disconnect 事件,服务端的这个连接也会销毁,导致客户端在缓存中保存的连接根本没用,所以这个方法行不通。取消一段时间重复进行请求也没用,使用闭包也没用。咱们考虑一下服务器,首先 nodejs 本身是 IO 密集型的,也就是可以抗住大量的请求事件,所以不用担心 CPU 的上升,因为只是暂时的,如果有一个人很缺德,就是要每天给服务器发请求,把它的ip加入黑名单。而且在客户端断开的时候,服务器也会及时清除这个连接上的socket,所以不会造成内存泄露的问题。所以这个问题可以变成如何优化服务器,可以使服务器处理大量并发请求,可以根据你服务器CPU的数量开启这个数值的nodejs服务线程数量,然后主线程将客户端发来的请求进行调度,平均分配到这几个nodejs线程上,就可以了
参考朴灵老师的书
- 我来看看朴灵老师说了点啥
- 单线程没有锁、线程同步问题,提高 CPU 利用率
- 主从模式:通过fork()复制的进程都是一个独立的进程,这个进程中有着独立而全新的V8实例。它需要至少30毫秒的启动时间和至少10 MB的内存,好在Node通过事件驱动的方式在单线程上解决了大并发的问题,这里启动多个进程只是为了充分将CPU资源利用起来,而不是为了解决并发问题(使用 epoll 监听文件描述符的数量决定了并发数),可以提高 CPU 利用率。尽快响应。让每个进程监听不同的端口,其中主进程监听主端口(如80),主进程对外接收所有的网络请求,再将这些请求分别代理到不同的端口的进程上。
- 对了,创建的是 HTTP 服务,也就是一个连接,并不是 listen accept 那种。而且设置了端口复用。意思就是在主线程上处理IO事件,在子线程上处理和客户端的数据收发事件。不能用所有线程都执行 http 服务,应该还有干别的的,输出日志啥的,这个线程数量要设计好。而且,主线程和子线程之间通信要消耗资源,要衡量线程同步、进程间通信这些,创建线程是否有优势,也可以对请求进行分类,IO密集和CPU密集执行不同的处理方式。一旦有异常出现,主进程会创建新的工作进程来为用户服务
- 在Node进程中不宜存放太多数据,因为它会加重垃圾回收的负担,进而影响性能
- 第三方进行数据存储解决数据共享
- cluster模块,用以解决多核CPU的利用率问题
- 在进程中判断是主进程还是工作进程,主要取决于环境变量中是否有NODE_UNIQUE_ID
- 它会在内部启动TCP服务器,在cluster.fork()子进程时,将这个TCP服务器端socket的文件描述符发送给工作进程。如果进程是通过cluster.fork()复制出来的,那么它的环境变量里就存在NODE_UNIQUE_ID,如果工作进程中存在listen()侦听网络端口的调用,它将拿到该文件描述符,通过SO_REUSEADDR端口重用,从而实现多个子进程共享端口。对于普通方式启动的进程,则不存在文件描述符传递共享等事情
- 对于自行通过child_process来操作时,则可以更灵活地控制工作进程,甚至控制多组工作进程