1. 缘起:推荐系统的新挑战

每当深夜打开短视频平台,系统总能精准推送我喜欢的猫咪视频。这种看似简单的推荐背后,是每秒处理百万级用户请求的复杂系统。传统技术栈在应对突发流量时常常捉襟见肘——就像春节期间的高铁售票系统,瞬间涌入的请求会让常规架构瞬间崩溃。

某次系统压力测试中,我们遭遇了这样的场景:当同时在线用户突破50万时,Python实现的推荐服务响应时间从200ms激增至3秒以上。这时,我们开始关注这个来自爱立信实验室的古老语言——Erlang。这个诞生于电信领域的语言,其并发模型就像精密的瑞士钟表,每个齿轮(进程)都独立运转却能完美协同。

2. Erlang技术栈速览

2.1 基因优势

Erlang的核心设计哲学体现在三方面:

  • 轻量级进程:每个进程仅需2KB内存,相当于用邮票大小的空间装下整个城市的人口
  • 消息传递机制:进程间通过消息通信,如同快递员传递包裹般可靠
  • OTP框架:预制了监督树、gen_server等模式,像乐高积木般可快速搭建健壮系统

2.2 推荐系统契合点

在内容推荐场景中,这些特性恰好解决关键痛点:

%% 用户画像更新服务示例
-module(user_profile).
-behaviour(gen_server).

%% 接口函数
update_preference(UserID, Action) ->
    gen_server:cast({via, pg2, ?MODULE}, {update, UserID, Action}).

%% 回调函数
handle_cast({update, UserID, Action}, State) ->
    NewProfile = calculate_weight(UserID, Action),
    ets:insert(user_profiles, {UserID, NewProfile}),
    {noreply, State}.

注释说明:

  1. gen_server提供标准的服务器模板
  2. pg2实现进程组管理,自动处理节点增减
  3. ets表实现毫秒级用户画像存取
  4. 异步cast调用避免阻塞主流程

3. 实战:推荐系统模块拆解

3.1 用户行为收集层

面对每秒10万+的点击事件,我们采用Erlang的监督树结构:

%% 监督树配置
init([]) ->
    Children = [
        {event_collector, 
         {gen_event, start_link, [{local, ?EVENT_HANDLER}]},
         permanent, 5000, worker, [dynamic]},
        {buffer_pool_sup,
         {supervisor, start_link, [?MODULE, buffer_pool_args]},
         transient, infinity, supervisor, []}
    ],
    {ok, {{one_for_one, 5, 10}, Children}}.

%% 事件处理器
handle_event({click, UserID, ItemID}, State) ->
    Buffer = get_buffer_pool(),
    buffer:write(Buffer, {UserID, ItemID, os:timestamp()}),
    {ok, State}.

技术亮点:

  • 进程池自动扩容:当缓冲区达到阈值时,监督者自动创建新的buffer进程
  • 背压控制:通过消息队列长度动态调整处理速度
  • 时间窗口聚合:每5秒将原始事件打包成批处理格式

3.2 实时推荐引擎

基于协同过滤的实时计算模块:

%% 相似度计算进程
calculate_similarity(ItemA, ItemB) ->
    receive
        {get_common_users, Requester} ->
            CommonUsers = find_common_users(ItemA, ItemB),
            Requester ! {result, jaccard_sim(CommonUsers)},
            calculate_similarity(ItemA, ItemB)
    end.

%% 推荐工作流
generate_recommendations(UserID) ->
    {ok, Profile} = get_profile(UserID),
    Candidates = find_candidate_items(Profile),
    Pids = [spawn_link(?MODULE, calculate_similarity, [Item, Profile#profile.favorite]) 
            || Item <- Candidates],
    Results = gather_scores(Pids, 200),  % 200ms超时
    sort_and_filter(Results).

关键技术:

  1. 动态进程派生:为每个候选物品创建独立计算进程
  2. 超时熔断机制:防止个别计算阻塞整个推荐流程
  3. 无锁并发:各相似度计算完全独立,无需竞争资源

4. 性能对比实验

在AWS c5.4xlarge实例上的测试数据:

指标 Erlang实现 Go实现 Java实现
10万QPS时CPU 62% 85% 92%
99分位延迟 45ms 78ms 105ms
故障恢复时间 200ms 1.2s 2.5s
内存占用 1.8GB 3.2GB 4.1GB

特别说明:Erlang的抢占式调度器在处理大量小请求时表现优异,但在需要进行复杂数值计算时(如矩阵分解),需要配合NIF调用C库。

5. 避坑指南

5.1 冷启动优化

初期版本在节点启动时遭遇过"惊群效应":

%% 错误示例:同时启动过多进程
init_database() ->
    [ets:new(T, [public, named_table]) || T <- [users, items, logs]], % 导致ETS表竞争
    spawn(fun() -> load_users() end),  % 无协调的并发加载
    spawn(fun() -> load_items() end).

优化方案:

%% 正确姿势:阶段化启动
start_phase(ets_tables, _, _) ->
    lists:foreach(fun(T) -> ets:new(T, [ordered_set, named_table]) end, 
                 [users, items, logs]).

start_phase(data_loading, _, _) ->
    supervisor:start_child(data_loader_sup, []). % 由监督者控制并发度

5.2 调试技巧

推荐使用Erlang的观察者工具:

# 启动WEB控制台
erl -name node@127.0.0.1 -setcookie mysecret -hidden
> observer:start().

通过该工具可以:

  • 实时查看进程消息队列堆积情况
  • 追踪特定用户的推荐流程
  • 分析热点函数调用

6. 技术选型建议

适用场景

  • 需要处理突发流量的新闻类推荐
  • 实时性要求高的短视频推荐
  • 多地域部署的全球化内容平台

不适用情况

  • 需要复杂机器学习训练的离线场景
  • 依赖大量GPU计算的视觉推荐
  • 已有成熟Java/Python团队维护的存量系统

7. 未来演进方向

我们正在尝试的混合架构:

[Erlang边缘节点] --gRPC--> [Python中心服务]
    ↑ 实时请求           ↓ 模型更新
[用户终端]              [TensorFlow集群]

这种架构下,Erlang负责处理90%的实时请求,Python中心服务每5分钟推送新的模型参数,兼顾了实时性和算法灵活性。

8. 总结与展望

经过两年实践,我们的Erlang推荐集群成功支撑了日均50亿次的推荐请求。一个有趣的发现是:Erlang进程的错误日志量只有原Java系统的1/20,这得益于其"任其崩溃"的设计哲学——坏掉的进程会被快速重启,而不是带着错误状态继续运行。

当然,这种架构对团队的技术栈适配提出了更高要求。我们内部流传着一个段子:新入职的工程师前两周都在学习《Erlang趣学指南》,第三周突然顿悟般喊道:"原来进程还可以这样玩!"