- 数据库
- 文档数据库
- 后端
【免费下载链接】couchdb
Seamless multi-primary syncing database with an intuitive HTTP/JSON API, designed for reliability
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 running
ddocswritten 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 = false2.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.由此可见有两种启用方式:
- 新方式(推荐):
enable_erlang_query_server = true; - 遗留方式:设置
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_update | Fun(NewDoc, OldDoc, UserCtx, SecObj) | 文档写入校验 |
rewrites | Fun(Req) | URL 重写规则 |
filters | Fun(Doc, Req) | 变更复制过滤 |
views(map) | Fun(Doc) | 视图 map |
shows | Fun(Doc, Req) | show 函数 |
updates | Fun(Doc, Req) | update 函数,返回[JsonDoc, JsonResp] |
lists | Fun(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
相关推荐
CouchDB 查询服务器(Query Server)完整配置指南:环境变量、进程池、Erlang 原生查询与 Mango/Search 调优
CouchDB 查询服务器(Query Server)完整配置指南:环境变量、进程池、Erlang 原生查询与 Mango/Search 调优 导读 查询服务器
数据库文档数据库后端Erlang Language Server:为Erlang开发者提供的强大语言服务
Erlang Language Server:为Erlang开发者提供的强大语言服务 Erlang Language Server(简称Erlang LS)是一
免费音乐下载神器:MusicDownload让你的音乐收藏从此无忧
免费音乐下载神器:MusicDownload让你的音乐收藏从此无忧 在数字音乐时代,你是否也曾为寻找高品质音乐资源而烦恼?MusicDownload作为一款终极
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考