node
node单线程,没有并发,但是可以利用cluster进行多cpu的利用。cluster是基于child_process的封装,帮你做了创建子进程,负载均衡,IPC的封装。
const cluster = require('cluster');
const http = require('http');
if (cluster.isMaster) {
let numReqs = 0;
setInterval(() => {
console.log(`numReqs = ${numReqs}`);
}, 1000);
function messageHandler(msg) {
if (msg.cmd && msg.cmd === 'notifyRequest') {
numReqs += 1;
}
}
const numCPUs = require('os').cpus().length;
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
for (const id in cluster.workers) {
cluster.workers[id].on('message', messageHandler);
}
} else {
// Worker processes have a http server.
http.Server((req, res) => {
res.writeHead(200);
res.end('hello world\n');
process.send({ cmd: 'notifyRequest' });
}).listen(8000);
}
我们通过cluster.fork()
来创造几个子进程,让子进程来替我们工作。在fork的时候会传一个参数到子进程,cluster.isMaster
就是根据有没有这个参数判断的。
如果是子进程就起一个server。
每个子进程都会绑定到8000端口,这不会引起端口占用吗?
答案是不会,因为listen并不会真的监听到8000端口,它会通过IPC把子进程的消息传到主进程,主进程会创建服务器,然后调用子进程的回调。
在子进程的回调中:子进程会根据主进程是否返回handle句柄来执行下一步的操作,如果没有handle句柄,说明在负载均衡的策略没有选中本进程。那么就自己造一个handle对象返回。
那自己造个对象怎么返回请求呢?
请求到主进程,主进程会分发请求,处理到该请求的子进程会通过IPC与主进程通信,这样就完成了一个请求的响应。
通过cluster完成单机器的负载均衡,那么多机器呢?还是得用nginx。
node & pm2
pm2 是node的进程管理工具,它封装了cluster,可以通过命令行来创建多个进程来处理。
写个config文件:
app.json
{
"name" : "app",
"script" : "src/main.js",
"watch" : true,
"merge_logs" : true,
"instances" : "max", // 使用cluster
"error_file" : "./log/error.log",
"out_file" : "./log/asccess.log",
"pid_file" : "./log/pid.pid",
"cwd" : "./",
"max_restarts" : 10,
"min_uptime": "10s",
"env": {
"NODE_ENV": "development",
"BABEL_ENV": "node"
},
"env_prod" : {
"NODE_ENV": "production"
}
}
pm2 start app.json
也可以不写配置文件直接写pm2 start -i 4 --name server index.js
开启4个instance。
通过参数开启多个子进程,而不需要修改我们的业务代码。
go
go也是非阻塞io,Golang默认所有的任务都在一个cpu核里,如果想使用多核来跑goroutine的任务,需要配置runtime.GOMAXPROCS。
自从Go 1.5开始, Go的GOMAXPROCS默认值已经设置为 CPU的核数,我们不用手动设置这个参数。
我们先说说go的并发。
go本身就可以通过go关键字来进行并发操作。go关键字创建的并发单元在go中叫goroutine。
比如:
package main
import (
"fmt"
"time",
// "runtime"
)
func main() {
go func(){
fmt.Println("123")
}()
go func(){
fmt.Println("456")
}()
// runtime.Gosched()
fmt.Println("789")
time.Sleep(time.Second)
}
会打印789 ,123,456,或者 780,456,123。
在主线程开始就通过go字段开启了2个goroutine,两个goroutine的执行顺序不确定。
如果当前goroutine不发生阻塞,它是不会让出CPU给其他goroutine的。
所以主线程不sleep,这两个goroutine都执行不了。
不过go提供runtime.Gosched()来达到让出CPU资源效果的函数,当然不是不执行,会在之后的某个时间段执行。如果把注释去掉,789就会最后执行。
单核的时候其实goroutine并不是真的“并行”,goroutine都在一个线程里,它们之间通过不停的让出时间片轮流运行,达到类似并行的效果。
如果我在123,或者456之前加 time.Sleep(time.Second)
。那么CPU的资源又会转让回主进程。
当一个goroutine发生阻塞,Go会自动地把与该goroutine处于同一系统线程的其他goroutines转移到另一个系统线程上去,以使这些goroutines不阻塞,主线程返回的时候goroutines又进入runqueue
下面这段代码:
1 package main
2
3 import (
4 "fmt"
5 "runtime"
6 )
7
8 var quit chan int = make(chan int)
9
10 func loop() {
11 for i := 0; i < 100; i++ { //为了观察,跑多些
12 fmt.Printf("%d ", i)
13 }
14 quit <- 0
15 }
16
17 func main() {
18 runtime.GOMAXPROCS(1)
19
20 go loop()
21 go loop()
22
23 for i := 0; i < 2; i++ {
24 <-quit
25 }
26 }
会打印什么呢?
runtime.GOMAXPROCS(2)
改成双核cpu,又会打印什么呢?
我们能看到,双核cpu的时候,goroutine会真正的并发执行而不是并行。他们会抢占式的执行。
参考https://studygolang.com/articles/1661