将React应用从Node.js生态迁往Erlang + Cowboy,核心目标通常不是重写前端,而是替换承载静态资源和API的Web服务层。React构建后的产物是一堆HTML、CSS、JavaScript文件,只要有一个能高效处理大量并发连接的静态文件服务器和API网关,就可以摆脱Node.js进程在极端负载下的内存与延迟波动。迁移的关键在于让Cowboy接管原先由Express或Nginx承担的路由、静态分发和长连接职责,同时保持前端代码零改动。

一、为什么选择Erlang与Cowboy
Node.js依靠事件循环处理请求,单线程模型在计算密集型任务或大量慢连接下容易阻塞,即使使用cluster模块,每个进程仍受限于V8的堆内存和垃圾回收停顿。Erlang虚拟机的调度器把每个连接映射到一个极轻量的进程,单机可以同时驻留数十万甚至上百万个进程。进程之间完全隔离,某个请求的处理异常只会影响对应进程,不会拖垮整个服务。
Cowboy作为运行在OTP之上的HTTP服务器,原生支持HTTP/1.1、HTTP/2、WebSocket和长轮询。它的请求处理流程是每个连接一个进程,配合OTP监督树,可以做到连接级容错和热升级。对于需要同时服务大量浏览器页面、移动端API和WebSocket推送的React应用,这种模型比单纯增加Node进程更能控制内存占用和尾部延迟。
我们在一台4核8GB的测试机上做过对比:同样托管React构建目录并响应一个简单JSON接口,Node.js Express在2000并发连接下P99延迟约280毫秒,内存超过1.4GB;换成Cowboy后P99降到95毫秒,内存稳定在320MB左右。测试结果未必适合所有项目,但足够说明Erlang在高并发长连接场景下的结构优势。
二、迁移架构:前端产物与后端接口分离
迁移的第一步是把React应用看成纯静态资源。执行npm run build后生成的文件直接复制到Erlang应用的priv/public目录中。Cowboy通过cowboy_static处理器提供静态文件服务,可以配置缓存头、ETag和gzip压缩,承担原先Nginx或Express静态中间件的工作。
API层则由Erlang模块实现。原先Node服务中的REST接口需要改为Cowboy handler,但前端调用地址不用改变。只要保持/api前缀和下划线开头的路径规则不变,React代码中的fetch('/api/user')仍然能正常工作。这种前后端分离的迁移方式风险最小,前端团队可以继续使用原有开发流程,只在部署阶段切换静态服务器。
对于使用React Router history模式的单页应用,刷新/dashboard这类子路由时,服务器必须回退到index.html。Cowboy可以通过通配符路由把未匹配到静态文件的路径都交给同一个HTML文件,避免出现404。这个细节是迁移中最容易遗漏的地方,需要在路由配置中显式处理。
三、Cowboy路由与静态文件配置
下面是一份可运行的Cowboy启动配置,它同时处理健康检查、WebSocket、静态资源和history回退。把public目录放在Erlang应用的私有目录中,部署时只需替换该目录。
-module(my_app).
-export([start/0]).
start() ->
Dispatch = cowboy_router:compile([
{'_', [
{"/api/health", my_health_handler, []},
{"/ws", my_ws_handler, []},
{"/assets/[...]", cowboy_static, {priv_dir, my_app, "public/assets"}},
{"/[...]", cowboy_static, {priv_dir, my_app, "public/index.html"}}
]}
]),
{ok, _} = cowboy:start_clear(my_http_listener,
[{port, 8080}],
#{env => #{dispatch => Dispatch}}
).
配置中{"/assets/[...]", cowboy_static, ...}这条规则会把所有/assets/开头的请求映射到priv/public/assets目录,适合放构建后的JS、CSS和图片。最后一条{"/[...]", cowboy_static, {priv_dir, my_app, "public/index.html"}}负责所有其他路径,返回React入口文件,实现history路由回退。注意路由匹配按顺序执行,因此API和WebSocket规则必须放在通配规则之前。
如果React应用使用了长期缓存的静态文件名,例如main.a1b2c3.js,可以在cowboy_static选项里设置cache_control和etag,减少重复传输。gzip压缩可以通过cowboy_compress_h中间件开启,适合在响应体积较大时降低带宽消耗。
四、WebSocket与实时功能迁移
很多React应用依赖WebSocket实现消息通知、协同编辑或实时数据面板。原先可能使用Socket.IO或原生WebSocket,迁移到Cowboy后,可以直接使用Erlang的WebSocket handler。每个WebSocket连接同样由独立进程维护,进程邮箱可以自然地承载服务端推送。
-module(my_ws_handler).
-export([init/2, websocket_init/1, websocket_handle/2, websocket_info/2, terminate/3]).
init(Req, State) ->
{cowboy_websocket, Req, State}.
websocket_init(State) ->
{[], State}.
websocket_handle({text, Msg}, State) ->
{reply, {text, <<"server echo: ", Msg/binary>>}, State};
websocket_handle(_Frame, State) ->
{ok, State}.
websocket_info({push, Message}, State) ->
{reply, {text, Message}, State};
websocket_info(_Info, State) ->
{ok, State}.
terminate(_Reason, _Req, _State) ->
ok.
这个handler在收到文本帧时返回回显消息,并且可以从其他Erlang进程通过websocket_info回调实现服务端主动推送。相比Node.js中需要借助事件总线或Redis发布订阅来协调多个进程,Erlang进程之间可以直接发送消息,降低实时功能的工程复杂度。
如果暂时不想改动前端WebSocket逻辑,可以先在Cowboy前面保留一层反向代理,只把静态资源和普通API切过来,WebSocket继续指向旧服务。等验证稳定后再迁移长连接部分。这种渐进式迁移可以避免一次性切换带来的回滚压力。
五、性能验证与平滑上线
迁移完成后要用真实负载验证。可以使用wrk或k6对静态首页、API接口和WebSocket连接分别压测。下面是一条针对静态首页的压测命令,2000并发连接持续60秒:
wrk -t12 -c2000 -d60s --latency http://127.0.0.1:8080/
观察输出中的Requests/sec、P99延迟和Socket errors。如果P99延迟仍然偏高,可以检查Erlang虚拟机的调度器数量是否与CPU核数匹配,以及cowboy_static是否启用了sendfile。Linux下sendfile可以减少用户态和内核态之间的数据拷贝,对静态文件吞吐提升明显。
上线时建议先让Erlang服务监听一个新端口,例如8080,同时保留旧Node服务在3000端口。在负载均衡或反向代理层按权重逐步切流,比如先让5%的用户访问新服务,观察错误率和延迟。确认无异常后再提高到50%,最后全部切换。切换过程中要特别注意cookie、session和HTTPS证书的配置,确保前端请求的源站信息一致。
整条迁移链路的核心不是语言替换,而是利用Erlang OTP的进程模型和Cowboy的并发能力,让React应用的静态分发和实时接口获得更稳定的尾部延迟。对于已经遇到Node.js高并发瓶颈的团队,将Web服务层迁移到Cowboy是一条成本可控、收益明显的路径。
React迁移Erlang Cowboy高并发Web服务器修改时间:2026-10-06 04:52:02