实现代理服务,最多见的即是代理服务器代理相应的协议体请求源站,并将响应从源站转发给客户端。而在本文的场景中,代理服务及源服务采用相同技术栈(Node.js),源服务是由代理服务fork出的业务服务(以下图),代理服务不只负责请求反向代理及转发规则设定,同时也负责业务服务伸缩扩容、日志输出与相关资源监控报警。下文称源服务为业务服务。
html
最初笔者采用上图的架构,业务服务为真正的HTTP服务或WebSocket服务,其侦听服务器的某个端口并处理代理服务的转发请求。可这有一些问题会困扰咱们:linux
所以,笔者尝试寻找更优的解决方案。git
老实说,以前学习linux网络编程的时候从没有尝试基于域套接字的HTTP Server,不过从协议上说,HTTP协议并无严格要求传输层协议必须为TCP,所以若是底层采用基于字节流的Unix Socket传输,应该也是能够实现要求的。github
同时相比较TCP协议实现的可靠传输,Unix Socket做为IPC有些优势:web
Unix Socket并非一种协议,它是进程间通讯(IPC)的一种方式,解决本机的两个进程通讯编程
在Node.js的http模块和net模块,都提供了相关接口 “listen(path, cb)”,不一样的是http模块在Unix Socket之上封装了HTTP的协议解析及相关规范,所以这是能够无缝兼容基于TCP实现的HTTP服务的。服务器
下为基于Unix Socket的HTTP Server与Client 样例:websocket
const http = require('http'); const path = require('path'); const fs = require('fs'); const p = path.join(__dirname,'tt.sock'); fs.unlinkSync(p); let s = http.createServer((req, res)=> { req.setEncoding('utf8') req.on('data',(d)=>{ console.log('server get:', d) }); res.end('helloworld!!!'); }); s.listen(p); setTimeout(()=>{ let c = http.request( { method: 'post', socketPath: p, path: '/test' }, (res) => { res.setEncoding('utf8'); res.on('data', (chunk) => { console.log(`响应主体: ${chunk}`); }); res.on('end', () => { }); }); c.write(JSON.stringify({abc: '12312312312'})); c.end(); },2000)
代理服务不只仅是代理请求,同时也负责业务服务进程的建立。在更为高级的需求下,代理服务同时也担负业务服务进程的扩容与伸缩,当业务流量上来时,为了提升业务服务的吞吐量,代理服务须要建立更多的业务服务进程,流量洪峰消散后回收适当的进程资源。透过这个角度会发现这种需求与cluster和child_process模块息息相关,所以下文会介绍业务服务集群的具体实现。网络
本文中的代理为了实现具备粘性session功能的WebSocket服务,所以采用了child_process模块建立业务进程。这里的粘性session主要指的是Socket.IO的握手报文须要始终与固定的进程进行协商,不然没法创建Socket.IO链接(此处Socket.IO链接特指Socket.IO成功运行之上的链接),具体可见个人文章 socket.io搭配pm2(cluster)集群解决方案 。不过,在fork业务进程的时候,会经过pre_hook脚本重写子进程的 http.Server.listen() 从而实现基于Unix Socket的底层可靠传输,这种方式则是参考了 cluster 模块对子进程的相关处理,关于cluster模块覆写子进程的listen,可参考个人另外一篇文章 Nodejs cluster模块深刻探究 的“多个子进程与端口复用”一节。session
// 子进程pre_hook脚本,实现基于Unix Socket可靠传输的HTTP Server function setupEnvironment() { process.title = 'ProxyNodeApp: ' + process['env']['APPNAME']; http.Server.prototype.originalListen = http.Server.prototype.listen; http.Server.prototype.listen = installServer; loadApplication(); } function installServer() { var server = this; var listenTries = 0; doListen(server, listenTries, extractCallback(arguments)); return server; } function doListen(server, listenTries, callback) { function errorHandler(error) { // error handle } // 生成pipe var socketPath = domainPath = generateServerSocketPath(); server.once('error', errorHandler); server.originalListen(socketPath, function() { server.removeListener('error', errorHandler); doneListening(server, callback); process.nextTick(finalizeStartup); }); process.send({ type: 'path', path: socketPath }); }
这样就完成了业务服务的底层基础设施,到了业务服务的编码阶段无需关注传输层的具体实现,仍然使用 http.Server.listen(${any_port})便可。此时业务服务侦放任何端口均可以,由于在传输层根本没有使用该端口,这样就避免了系统端口的浪费。
流量转发包括了HTTP请求和WebSocket握手报文,虽然WebSocket握手报文仍然是基于HTTP协议实现,但须要不一样的处理,所以这里分开来讲。
此节可参考 “基于Unix Socket的HTTP Server与Client”的示例,在代理服务中新建立基于Unix Socket的HTTP client请求业务服务,同时将响应pipe给客户端。
class Client extends EventEmitter{ constructor(options) { super(); options = options || {}; this.originHttpSocket = options.originHttpSocket; this.res = options.res; this.rej = options.rej; if (options.socket) { this.socket = options.socket; } else { let self = this; this.socket = http.request({ method: self.originHttpSocket.method, socketPath: options.sockPath, path: self.originHttpSocket.url, headers: self.originHttpSocket.headers }, (res) => { self.originHttpSocket.set(res.headers); self.originHttpSocket.res.writeHead(res.statusCode); // 代理响应 res.pipe(self.originHttpSocket.res) self.res(); }); } } send() { // 代理请求 this.originHttpSocket.req.pipe(this.socket); } } // proxy server const app = new koa(); app.use(async ctx => { await new Promise((res,rej) => { // 代理请求 let client = new Client({ originHttpSocket: ctx, sockPath: domainPath, res, rej }); client.send(); }); }); let server = app.listen(8000);
若是不作WebSocket报文处理,到此为止采用Socket.IO仅仅可使用 “polling” 模式,即经过XHR轮询的形式实现假的长链接,WebSocket链接没法创建。所以,若是为了更好性能体验,须要处理WebSocket报文。这里主要参考了“http-proxy”的实现,针对报文作了一些操做:
报文处理的核心在于第2点:建立一个代理服务与业务服务进程之间的“长链接”(该链接时基于Unix Socket管道的,而非TCP长链接),并使用此链接overlay的HTTP升级请求进行协议升级。
此处实现较为复杂,所以只呈现代理服务的处理,关于WebSocket报文处理的详细过程,可参考 proxy-based-unixsocket。
// 初始化ws模块 wsHandler = new WsHandler({ target: { socketPath: domainPath } }, (err, req, socket) => { console.error(`代理wsHandler出错`, err); }); // 代理ws协议握手升级 server.on('upgrade',(req, socket, head) =>{ wsHandler.ws(req, socket, head); });
你们都知道,在Node.js范畴实现HTTP服务集群,应该使用cluster模块而不是“child_process”模块,这是由于采用child_process实现的HTTP服务集群会出现调度上不均匀的问题(内核为了节省上下文切换开销作出来的“优化之举”,详情可参考 Nodejs cluster模块深刻探究“请求分发策略”一节)。可为什么在本文的实现中仍采用child_process模块呢?
答案是:场景不一样。做为代理服务,它可使用cluster模块实现代理服务的集群;而针对业务服务,在session的场景中须要由代理服实现对应的转发策略,其余状况则采用RoundRobin策略便可,所以child_process模块更为合适。
本文并未实现代理服务的负载均衡策略,其实现仍然在 Nodejs cluster模块深刻探究 中讲述,所以可参阅此文。
最终,在保持进程模型稳定的前提下,变动了底层协议可实现更高性能的代理服务。
本文代码proxy-based-unixsocket。