編寫自己的js運(yùn)行時(shí)第二篇
前言:第一版基于V8實(shí)現(xiàn)了一個(gè)樸素版的服務(wù)器,第二版支持了多進(jìn)程架構(gòu),并且支持了SO_REUSEPORT。本文介紹一下第二版的一些實(shí)現(xiàn),設(shè)計(jì)上還是比較隨意的,目前主要關(guān)注功能。
首先我們看看第二版怎么使用。
1 通過fork共享端口
- const TCPServer = TCP();
- const tcpServer = new TCPServer('127.0.0.1', 8989);
- tcpServer.socket();
- tcpServer.setReusePort(1);
- tcpServer.bind();
- tcpServer.listen();
- for (let i = 0; i < 3; i++) {
- // 等于0說明是子進(jìn)程,進(jìn)入處理連接的邏輯,否則是主進(jìn)程,循環(huán)創(chuàng)建多個(gè)進(jìn)程
- if (Child_Process.fork() === 0) {
- while(1) {
- tcpServer.accept();
- }
- }
- }
- // 主進(jìn)程創(chuàng)建完子進(jìn)程后自己進(jìn)入阻塞狀態(tài)
- Child_Process.wait();
通過fork共享端口版本的原理是主進(jìn)程首先創(chuàng)建一個(gè)socket并且綁定一個(gè)端口。然后通過fork的方式讓多個(gè)子進(jìn)程共享監(jiān)聽的端口。最后主進(jìn)程進(jìn)入阻塞模式。核心實(shí)現(xiàn)是fork,我們看看代碼。
- static Local<Object> ChildProcess(Isolate * isolate) {
- Local<ObjectTemplate> target = ObjectTemplate::New(isolate);
- Local<String> forkName = String::NewFromUtf8(isolate, "fork", NewStringType::kNormal, strlen("fork")).ToLocalChecked();
- Local<String> waitName = String::NewFromUtf8(isolate, "wait", NewStringType::kNormal, strlen("wait")).ToLocalChecked();
- target->Set(forkName, FunctionTemplate::New(isolate, Child_Process::Fork));
- target->Set(waitName, FunctionTemplate::New(isolate, Child_Process::Wait));
- Local<Object> obj;
- bool ignore = target->NewInstance(isolate->GetCurrentContext()).ToLocal(&obj);
- return obj;
- }
第二版加入了進(jìn)程模塊,上面的代碼定義了進(jìn)程模塊的功能。然后注入到全局變量,No.js目前的設(shè)計(jì)中,每個(gè)模塊是一個(gè)全局變量,和我們使用Object、Array一樣,不像Node.js的C++模塊是鏈成一條鏈表。
- // 模塊名稱
- Local<Value> child_process_name = String::NewFromUtf8(isolate, "Child_Process", strlen("Child_Process")).ToLocalChecked();// 注冊全局變量
- global->Set(context, child_process_name, ChildProcess(isolate));
這樣就完成了模塊的注入,在JS層就可以使用了。下面我們看看具體的實(shí)現(xiàn)。
- class Child_Process {
- public:
- static void Fork(const FunctionCallbackInfo<Value>& info) {
- info.GetReturnValue().Set(Number::New(info.GetIsolate(), fork()));
- }
- static void Wait(const FunctionCallbackInfo<Value>& info) {
- int status;
- wait(&status);
- }
- };
實(shí)現(xiàn)很簡單,只是對fork函數(shù)的封裝,重點(diǎn)在于對fork函數(shù)的理解, 執(zhí)行fork函數(shù)后會創(chuàng)建一個(gè)子進(jìn)程,子進(jìn)程的fork返回0,主進(jìn)程返回子進(jìn)程id,通過這個(gè)特性,我們可以寫一個(gè)if判斷處理下一步的邏輯。
2 通過fork+execve+reuserport共享端口
第二種模式是比較復(fù)雜且比較高性能的模式,之前的文章介紹過不同服務(wù)器架構(gòu)的實(shí)現(xiàn)和優(yōu)缺點(diǎn),第一種fork共享端口的模式中,會有驚群和負(fù)載不均衡的問題,有興趣可以參考之前的文章,就不多介紹。接下來看第二種模式的使用(下面代碼是execve-server.js)。
- const TCPServer = TCP();
- const tcpServer = new TCPServer('127.0.0.1', 8989);
- tcpServer.socket();
- tcpServer.setReusePort(1);
- tcpServer.bind();
- tcpServer.listen();
- const isMaster = Child_Process.getEnv("isMaster") === "";
- if (isMaster) {
- for (let i = 0; i < 3; i++) {
- Child_Process.execve("./No", "execve-server.js");
- }
- Child_Process.wait();
- } else {
- while(1) {
- tcpServer.accept();
- }
- }
我們知道多個(gè)進(jìn)程是不能綁定同一個(gè)端口的,第一種模式中通過fork繞過了這個(gè)限制,第二版面對并解決了這個(gè)問題。上面代碼的邏輯看起來也很簡單,主進(jìn)程創(chuàng)建多個(gè)子進(jìn)程,并且在每個(gè)子進(jìn)程里執(zhí)行同一個(gè)文件execve-server.js。然后在execve-server.js中通過環(huán)境變量isMaster區(qū)分主子進(jìn)程進(jìn)行不同的處理,當(dāng)然也可以執(zhí)行新的文件。這里是為了提到isMaster這個(gè)環(huán)境變量。上面代碼中,重點(diǎn)是setReusePort和execve,下面我們具體看一下實(shí)現(xiàn)。
- static Local<Object> ChildProcess(Isolate * isolate) {
- Local<ObjectTemplate> target = ObjectTemplate::New(isolate);
- Local<String> execveName = String::NewFromUtf8(isolate, "execve", NewStringType::kNormal, strlen("execve")).ToLocalChecked();
- Local<String> getEnvName = String::NewFromUtf8(isolate, "getEnv", NewStringType::kNormal, strlen("getEnv")).ToLocalChecked();
- target->Set(execveName, FunctionTemplate::New(isolate, Child_Process::Execve));
- target->Set(getEnvName, FunctionTemplate::New(isolate, Child_Process::GetEnv));
- Local<Object> obj;
- bool ignore = target->NewInstance(isolate->GetCurrentContext()).ToLocal(&obj);
- return obj;
- }
同樣,先定義入口使得JS可以調(diào)用。另外給TCP模塊定義了一個(gè)新接口setReusePort。
- SetProtoMethod(isolate, TCPServer, "setReusePort", TCPServer::TCPServerSetUserPort);
接下來看底層的實(shí)現(xiàn),首先看TCPServerSetUserPort的實(shí)現(xiàn)。
- static void TCPServerSetUserPort(const FunctionCallbackInfo<Value>& info) {
- int on = info[0].As<Uint32>()->Value();
- GetTCPServer(info.Holder())->Setsockopt(SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));
- }
- int Setsockopt(int level, int optionName, const void *optionValue, socklen_t option_len) {
- return setsockopt(listerFd, level, optionName, optionValue, option_len);
- }
簡單地對setsockopt的封裝,沒有太多需要講的。下面看環(huán)境變量和execve的邏輯。
- class Child_Process {
- public:
- static void GetEnv(const FunctionCallbackInfo<Value>& info) {
- String::Utf8Value key(info.GetIsolate(), info[0]);
- char * value = getenv(*key);
- // Logger::log(value);
- Local<String> str = String::NewFromUtf8(info.GetIsolate(), value, NewStringType::kNormal, strlen(value)).ToLocalChecked();
- info.GetReturnValue().Set(str);
- }
- static void Execve(const FunctionCallbackInfo<Value>& info) {
- int length = info.Length();
- char** args = new char*[length + 1];
- int i = 0;
- for (i = 0; i < length; i++) {
- String::Utf8Value arg(info.GetIsolate(), info[i]);
- args[i] = strdup(*arg);
- }
- args[i] = NULL;
- char *env[] = { "isMaster=0", NULL };
- // int fd[2];
- // socketpair(AF_UNIX, SOCK_STREAM, 0, fd);
- int pid = fork();
- if (pid == 0) {
- // close(fd[0]);
- execve(args[0], args, env);
- // execve會加載可執(zhí)行文件,從新的入口開始執(zhí)行,執(zhí)行到這說明execve出錯了
- write(1, strerror(errno), sizeof(strerror(errno)));
- exit(-1);
- }
- // close(fd[1]);
- if (args) {
- for (int i = 0; i < length && args[i]; i++) {
- free(args[i]);
- }
- delete [] args;
- }
- }
- };
目前只實(shí)現(xiàn)了獲取環(huán)境變量的邏輯,主要是對getenv的封裝。execve的代碼看起來很多,主要是參數(shù)的處理,我們只需要關(guān)注下面的代碼。
- int pid = fork();
- // 子進(jìn)程重新加載新的可執(zhí)行文件
- if (pid == 0) {
- // close(fd[0]);
- execve(args[0], args, env);
- // execve會加載可執(zhí)行文件,從新的入口開始執(zhí)行,執(zhí)行到這說明execve出錯了
- write(1, strerror(errno), sizeof(strerror(errno)));
- exit(-1);
- }
首先通過fork創(chuàng)建一個(gè)子進(jìn)程,然后通過execve加載要執(zhí)行的代碼(這里是./No execve-server.js)。重點(diǎn)是execve函數(shù)會重新加載可執(zhí)行文件,然后從新的地址(可執(zhí)行文件中指定)開始執(zhí)行,所以我們看到execve后是不需要return的,因?yàn)橄旅娴拇a不會執(zhí)行了,除非execve執(zhí)行出錯了,這里我們打印錯誤信息然后退出進(jìn)程。第二種模式的好處就是我們可以隨意在多個(gè)js文件中綁定同一個(gè)端口而不會報(bào)錯,這得益于SO_REUSEPORT的特性。SO_REUSEPORT讓每個(gè)進(jìn)程對應(yīng)一個(gè)連接隊(duì)列,解決了驚群問題,并且內(nèi)核負(fù)責(zé)連接分發(fā)的復(fù)雜均衡,不僅提高了性能,同時(shí)使得應(yīng)用程序變得簡單。
3 和Node.js相比
Node.js的進(jìn)程是通過fork+execve實(shí)現(xiàn)的,Cluster模塊基于進(jìn)程模塊實(shí)現(xiàn)了多進(jìn)程架構(gòu),主要有兩種模式:輪詢和共享,輪詢就是主進(jìn)程接收連接分發(fā)給子進(jìn)程處理,子進(jìn)程不接收連接只負(fù)責(zé)處理業(yè)務(wù)邏輯。這種模式的好處是沒有驚群現(xiàn)象,但是主進(jìn)程的能力會成為服務(wù)器的瓶頸,共享模式和本文的第一種一樣,多個(gè)子進(jìn)程共享一個(gè)端口,但是實(shí)現(xiàn)不一樣,本文是主進(jìn)程創(chuàng)建socket通過fork子進(jìn)程共享,Node.js是主進(jìn)程創(chuàng)建socket通過文件描述符的方式傳遞給子進(jìn)程,不過殊途同歸,主要是讓多個(gè)子進(jìn)程共享監(jiān)聽socket。本文的第二種模式,目前Node.js還不支持,因?yàn)镾O_REUSEPORT是比較新的特性,但是對性能提升非常大。
后記:以上就是第二版新增的功能,我們已經(jīng)具備了一個(gè)可以處理請求的多進(jìn)程架構(gòu)服務(wù)器,但是目前還是單進(jìn)程里串行處理請求的,我們還需要很多東西,文件、IPC、事件驅(qū)動模塊、HTTP解析器等等,后續(xù)會考慮把最近寫的Node.js io_uring Addon合進(jìn)來。最近把頭文件和V8靜態(tài)庫都打包了,有興趣的同學(xué)可以自行編譯運(yùn)行https://github.com/theanarkh/No.js。