news 2026/10/9 10:06:48

CouchDB 原生 Erlang 查询服务器(Native Erlang Query Server)完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CouchDB 原生 Erlang 查询服务器(Native Erlang Query Server)完整指南
  • 数据库
  • 文档数据库
  • 后端

【免费下载链接】couchdb

Seamless multi-primary syncing database with an intuitive HTTP/JSON API, designed for reliability

项目地址:https://gitcode.com/gh_mirrors/co/couchdb
点击查看免费下载

CouchDB 默认通过外部进程(Query Server)执行设计文档(Design Document)中的函数,例如 JavaScript 视图。而原生 Erlang 查询服务器(Native Erlang Query Server)允许你直接用 Erlang 编写 map/reduce、list、show、filter 等函数,让它们以 Erlang 函数的形式直接运行在 CouchDB 虚拟机内部,绕过 stdio 通信与 JSON 序列化/反序列化的往返开销。本文以官方文档 erlang.rst 为主体,结合仓库源码详细讲解其启用方式、运行时 API(Emit、FoldRows、GetRow、Log、Send、Start)与底层实现原理,帮助你安全、正确地用 Erlang 编写高性能的 CouchDB 视图与列表函数。

1. 什么是原生 Erlang 查询服务器

CouchDB 将设计文档中函数的计算委托给外部查询服务器——一种通过标准输入/输出与 CouchDB 通信、使用基于行的 JSON 消息协议的独立 OS 进程(参见 query-servers.rst)。默认查询服务器由 JavaScript 编写,通过 SpiderMonkey/QuickJS 引擎运行。

与之不同,CouchDB 还内置了一个原生(Native)Erlang 查询服务器:核心模块为 couch_native_process.erl,它以gen_server形式运行在 CouchDB BEAM 虚拟机内部。正如 query-servers.rst 中的说明:

The Native Erlang Query Server allows runningddocswritten in Erlang natively, bypassing stdio communication and JSON serialization/deserialization round trip overhead.

也就是说,Erlang 函数既不经过外部进程,也不经过 JSON 编解码,而是直接以 Erlang 闭包的形式在 CouchDB 进程内执行,因此比 JavaScript 函数更快(官方文档原文:"Erlang functions are faster than JavaScript ones")。

重要前提:Erlang 查询服务器默认是关闭的(disabled by default)。这是出于安全考虑——不同于 JavaScript 查询服务器运行在沙箱中,Erlang 查询服务器不在沙箱模式下运行,Erlang 代码对 OS、文件系统和网络拥有完全访问权限,可能带来安全隐患。官方文档 erlang.rst 开篇即提示:

The Erlang query server is disabled by default. Read configuration guide about reasons why and how to enable it.

因此,启用前请务必评估运行代码的来源可信度,尤其是他人编写的函数。

2. 启用与配置

2.1 修改 local.ini 启用

在local.ini中添加[native_query_servers]小节并设置开关:

[native_query_servers] enable_erlang_query_server = true

修改后需要重启 CouchDB 服务才能生效。

在默认配置 default.ini 中,该选项以注释形式存在,默认值为false:

[native_query_servers] ;enable_erlang_query_server = false

2.2 源码中的启用判定逻辑

在 couch_proc_manager.erl 中可以看到启用判定与注册逻辑:

native_query_server_enabled() -> % 1. [native_query_server] enable_erlang_query_server = true | false % 2. if [native_query_server] erlang == {couch_native_process, start_link, []} -> pretend true as well NativeEnabled = config:get_boolean("native_query_servers", "enable_erlang_query_server", false), NativeLegacyConfig = config:get("native_query_servers", "erlang", ""), NativeLegacyEnabled = NativeLegacyConfig =:= "{couch_native_process, start_link, []}", NativeEnabled orelse NativeLegacyEnabled. maybe_configure_erlang_native_servers() -> case native_query_server_enabled() of true -> ets:insert(?SERVERS, [ {"ERLANG", {couch_native_process, start_link, []}} ]); _Else -> ok end.

由此可见有两种启用方式:

  1. 新方式(推荐):enable_erlang_query_server = true;
  2. 遗留方式:设置erlang = {couch_native_process, start_link, []},此时会被视为已启用(源码注释称为 "pretend true as well")。

启用后,ERLANG语言被注册为{couch_native_process, start_link, []},即直接在 CouchDB 内部启动couch_native_process的 gen_server。而其他外部查询服务器(如 JavaScript)则是通过couch_os_process:start_link(Command)启动 OS 进程(见 couch_proc_manager.erl)。

2.3 在查询中使用 Erlang

设计文档通过language字段声明使用的查询服务器语言(参见 ddocs.rst)。将language设为"erlang"后,该设计文档中的 map、reduce、list、show、filter 等函数均以 Erlang 源码字符串形式存储,并由原生 Erlang 查询服务器执行。

3. 一个完整的 map/reduce 示例

官方文档给出一个统计"每个修订版本号对应的文档数量"的经典示例。先向数据库添加若干文档,然后创建视图:

%% Map Function fun({Doc}) -> <<K,_/binary>> = proplists:get_value(<<"_rev">>, Doc, null), V = proplists:get_value(<<"_id">>, Doc, null), Emit(<<K>>, V) end. %% Reduce Function fun(Keys, Values, ReReduce) -> length(Values) end.

Map 函数接收一个文档(以 Erlang proplist 形式表示),从_rev字段中提取修订号前缀(<<K,_/binary>>模式匹配出1-、2-等版本号),以修订号为 key、_id为 value 调用Emit发射键值对;Reduce 函数则简单地返回length(Values),即每个修订号下的文档总数。视图运行成功后,即可看到每个修订版本号对应的文档数量列表。

4. 运行时 API 详解

原生 Erlang 查询服务器向你的 Erlang 函数注入一组内置绑定函数(binding)。这些绑定的实际定义位于 couch_native_process.erl 的bindings/2,3函数中。下面逐一讲解官方文档定义的六个 API。

4.1 Emit(Id, Value)

向视图索引进程发射key-value键值对,是 map 函数的核心输出手段。

fun({Doc}) -> <<K,_/binary>> = proplists:get_value(<<"_rev">>, Doc, null), V = proplists:get_value(<<"_id">>, Doc, null), Emit(<<K>>, V) end.

底层实现中,每次Emit调用都会把[Id, Value]追加到以函数签名为 key 的进程字典列表中(见 couch_native_process.erl):

Emit = fun(Id, Value) -> Curr = erlang:get(Sig), erlang:put(Sig, [[Id, Value] | Curr]) end,

这也解释了为什么同一个文档可以多次调用Emit——每次调用都会产生一条独立的索引记录,最终在map_doc命令处理时通过lists:reverse(erlang:get(Sig))返回全部发射结果(见 couch_native_process.erl)。

4.2 FoldRows(Fun, Acc)

用于在 list 函数中迭代视图的所有行。Fun是处理函数对象,Acc是Fun上一次返回的累加值。

官方文档示例——逐行打印前一个与当前文档 id:

fun(Head, {Req}) -> Fun = fun({Row}, Acc) -> Id = couch_util:get_value(<<"id">>, Row), Send(list_to_binary(io_lib:format("Previous doc id: ~p~n", [Acc]))), Send(list_to_binary(io_lib:format("Current doc id: ~p~n", [Id]))), {ok, Id} end, FoldRows(Fun, nil), "" end.

注意内部函数必须以{ok, NewAcc}返回继续迭代,或以{stop, NewAcc}提前终止迭代;首次调用时Acc传入nil。

4.3 GetRow()

从相关视图结果中取出下一行(row)。FoldRows的底层实现正是基于GetRow递归构建的,官方文档给出了其背景实现(对应源码 couch_native_process.erl):

foldrows(GetRow, ProcRow, Acc) -> case GetRow() of nil -> {ok, Acc}; Row -> case (catch ProcRow(Row, Acc)) of {ok, Acc2} -> foldrows(GetRow, ProcRow, Acc2); {stop, Acc2} -> {ok, Acc2} end end.

GetRow的注入实现见 couch_native_process.erl:它会先把已累积的Send分块通过{self(), chunks, ...}消息发出(start_list_resp负责触发 list 的start响应),然后阻塞等待{Self, list_row, Row}或{Self, list_end}消息;若超过进程超时时间(默认 5000ms,来自evstate.timeout)则抛出{timeout, list_pid_getrow}。

4.4 Log(Msg)

以INFO级别记录一条日志消息。官方文档示例在 map 函数中记录文档 id:

fun({Doc}) -> <<K,_/binary>> = proplists:get_value(<<"_rev">>, Doc, null), V = proplists:get_value(<<"_id">>, Doc, null), Log(lists:flatten(io_lib:format("Hello from ~s doc!", [V]))), Emit(<<K>>, V) end.

map 函数运行后,CouchDB 日志(例如/var/log/couchdb/couch.log)中会出现如下记录:

[Sun, 04 Nov 2012 11:33:58 GMT] [info] [<0.9144.2>] Hello from 8d300b86622d67953d102165dbe99467 doc!

源码中Log绑定直接转发到couch_log:info(Msg, [])(见 couch_native_process.erl),因此消息会进入 CouchDB 的标准日志体系,可用于在批量索引时输出调试信息。

4.5 Send(Chunk)

向响应中发送单个字符串分块Chunk,用于 list 函数逐块输出内容。

fun(Head, {Req}) -> Send("Hello,"), Send(" "), Send("Couch"), "!" end.

上述函数产生的响应为:

Hello, Couch!

实现上,Send与Emit类似,把分块累积到进程字典,并在 list 结束时通过lists:reverse汇总返回(见 couch_native_process.erl)。list 函数的最后一个表达式作为最后的输出分块(此处为"!"),与前面Send的分块拼接成完整响应。

4.6 Start(Headers)

初始化 list 函数的响应头。Headers是响应对象(response object)的 proplist。在这个阶段可以定义响应状态码和响应头。官方文档示例——返回 302 重定向到 CouchDB 官网:

fun(Head, {Req}) -> Start({[{<<"code">>, 302}, {<<"headers">>, {[ {<<"Location">>, <<"http://couchdb.apache.org">>}] }} ]}), "Relax!" end.

实现中Start把响应头存入进程字典的list_headers(见 couch_native_process.erl),随后在start_list_resp/2中作为start响应的组成部分发送给 CouchDB(见 couch_native_process.erl);若未调用Start,则默认使用{[{<<"headers">>, {[]}}]}空响应头。响应对象(含code、headers等字段)的完整定义可参见设计文档相关章节 ddocs.rst。

4.7 其他可用绑定

除了文档中列出的六个 API,源码bindings/3还注入了一个DDoc绑定:当执行涉及设计文档的函数(如 validate_doc_update、show、list)时,会把{'DDoc', DDoc}加入绑定列表(见 couch_native_process.erl),使 Erlang 函数能够访问当前设计文档的完整内容。

5. 底层原理:Erlang 源码如何被编译执行

原生查询服务器的一个关键机制是动态编译用户提供的 Erlang 源码字符串。在 couch_native_process.erl 的makefun/3中可以看到完整流程:

makefun(_State, Source, BindFuns) when is_list(BindFuns) -> FunStr = binary_to_list(Source), {ok, Tokens, _} = erl_scan:string(FunStr), Form = case (catch erl_parse:parse_exprs(Tokens)) of {ok, [ParsedForm]} -> ParsedForm; ... end, Bindings = lists:foldl( fun({Name, Fun}, Acc) -> erl_eval:add_binding(Name, Fun, Acc) end, erl_eval:new_bindings(), BindFuns ), {value, Fun, _} = erl_eval:expr(Form, Bindings), Fun.

流程为:erl_scan:string对源码做词法分析 →erl_parse:parse_exprs解析为抽象语法树 → 通过erl_eval:add_binding将Emit、Log、Send等绑定函数注入求值环境 →erl_eval:expr求值得到真正的 Erlang 函数闭包。每个函数都会基于源码计算 MD5 签名(couch_hash:md5_hash(Source)),该签名作为进程字典中累积发射结果的 key,同时用于缓存与去重(见 couch_native_process.erl)。

5.1 支持的设计函数类型

从ddoc/3的分发逻辑(couch_native_process.erl)可以看出,原生 Erlang 查询服务器完整支持以下设计函数:

函数类型调用形态说明
validate_doc_updateFun(NewDoc, OldDoc, UserCtx, SecObj)文档写入校验
rewritesFun(Req)URL 重写规则
filtersFun(Doc, Req)变更复制过滤
views(map)Fun(Doc)视图 map
showsFun(Doc, Req)show 函数
updatesFun(Doc, Req)update 函数,返回[JsonDoc, JsonResp]
listsFun(Head, Req)list 函数,配合Start/Send/GetRow/FoldRows使用

list 函数会被放入独立 spawn 的进程中执行(spawn_link),并借助消息传递与GetRow实现"拉取式"逐行读取视图结果(见 couch_native_process.erl)。

6. 相关查询服务器配置参数

虽然 Erlang 查询服务器运行在 CouchDB 内部,但它仍属于查询服务器体系,受 query_server_config 小节中通用参数的影响:

[query_server_config] commit_freq = 5 ; 视图索引变更落盘延迟(秒),默认 5 os_process_limit = 100 ; 查询服务器 OS 进程硬上限,默认 100 os_process_soft_limit = 100 ; 查询服务器 OS 进程软上限,默认 100 reduce_limit = true ; Reduce 溢出控制,默认 true

其中os_process_limit/os_process_soft_limit主要约束外部 OS 进程型查询服务器;对于原生 Erlang 查询服务器,进程池的获取/归还同样经由 couch_proc_manager.erl 管理,enable_erlang_query_server开关决定ERLANG是否出现在进程池可用的语言注册表中。reduce_limit则控制Reduce overflow错误:当 reduce 函数输出比输入还大时抛出错误(见 couch_query_servers.erl 中check_sum_overflow对OutSize > 4906且OutSize * 2 > InSize的判定)。

7. 安全权衡与适用建议

最后,再次强调官方文档反复提醒的安全要点:

  • 默认禁用:出于安全限制,Erlang 查询服务器默认不启用(default.ini 中enable_erlang_query_server默认false)。
  • 无沙箱:与 JavaScript 查询服务器不同,Erlang 查询服务器不运行在沙箱模式,Erlang 代码对 OS、文件系统和网络拥有完全访问权限,可能引发安全问题。
  • 性能优势:Erlang 函数比 JavaScript 函数更快,因为它绕过了 stdio 通信和 JSON 编解码开销。
  • 使用建议:仅在信任代码来源的前提下启用,避免运行他人编写的未知 Erlang 设计函数;生产环境中如需类似能力,可考虑在受控环境中对设计文档的提交进行审计。

从源码结构看,couch_native_process的设计目标正如其模块注释所述:"提供最小可用的原生视图服务器(the smallest possible native view-server)"——它暴露了足以让 Erlang 服务器充当完整视图服务器的函数,但不包含额外的辅助函数,期望第三方扩展在其之上构建更友好的封装层(couch_native_process.erl)。理解这一层边界,有助于你正确评估在什么场景下使用原生 Erlang 查询服务器,以及如何与 JavaScript 查询服务器、Mango 查询引擎进行组合选型。

  • 数据库
  • 文档数据库
  • 后端

【免费下载链接】couchdb

Seamless multi-primary syncing database with an intuitive HTTP/JSON API, designed for reliability

项目地址:https://gitcode.com/gh_mirrors/co/couchdb
点击查看免费下载
上一篇:番茄小说下载工具快速上手:一个ID换整本小说,5种格式、3种运行方式全讲清
下一篇:抖音批量下载完整指南:把一个博主的 100 条作品搬进本地

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 10:06:35

Transformer位置编码原理与实战选型指南

1. 位置编码到底在解决什么问题&#xff1f;——别再把它当成“加个向量”就完事了你刚接触Transformer时&#xff0c;大概率被这句话绕晕过&#xff1a;“Self-Attention本身不具备位置感知能力&#xff0c;所以必须引入位置编码。”但这句话背后藏着一个关键矛盾&#xff1a;…

作者头像 李华
网站建设 2026/10/9 10:06:34

编译原理实验:语法分析程序设计与实现全攻略

简介&#xff1a;这份资源是面向计算机专业学生的编译原理实验配套文档&#xff0c;聚焦语法分析程序的设计与实现&#xff0c;适合正在完成实验二、需要参考完整实现思路与代码的学习者。文档以算术表达式简化子集为分析对象&#xff0c;系统梳理了实验目的、BNF文法定义、LL(…

作者头像 李华
网站建设 2026/10/9 10:03:32

渔具制造“隐形冠军”乐欣户外闯关港股IPO,8个月进账4.6亿

乐欣户外通过上市聆讯&#xff0c;这条消息从上周五开始就在户外产业圈子里传开了。披露出来的核心数据确实很有话题性&#xff1a;8个月营收4.6亿元&#xff0c;净利润5624万元。单看这两个数字&#xff0c;放在A股那些动辄几十亿营收的制造企业面前不算起眼&#xff0c;但你要…

作者头像 李华
网站建设 2026/10/9 10:03:32

基于Spring Boot的企业活动中心场地预约管理系统设计与实现

1. 选题拆解&#xff1a;这套预约系统到底值不值得做先说结论&#xff1a;基于 Spring Boot 的企业活动中心场地预约管理系统&#xff0c;是我近几年见过最适合拿来做 Java 毕设的选题之一&#xff0c;甚至可以说它是“管理信息系统 预约场景 前后端分离”这三件事的一次标准…

作者头像 李华
网站建设 2026/10/9 9:59:16

导航多边形平面化:空间计算不可绕过的底层铁律

1. 为什么“多边形必须平面化”不是技术偏好&#xff0c;而是空间计算的底层铁律&#xff1f;你有没有遇到过这样的情况&#xff1a;在做室内定位系统时&#xff0c;明明所有传感器数据都校准过了&#xff0c;路径规划模块却总在某个拐角处突然“跳点”&#xff0c;生成一条穿墙…

作者头像 李华