田春华 博士作者

RServe源代码解析(上)

本文对Rserve的设计初衷、运行机制、核心对象和方法进行粗浅探讨,尝试为利用Rserve做深度产品开发的开发者提供初始帮助。

本文对Rserve的设计初衷、运行机制、核心对象和方法进行粗浅探讨,尝试为利用Rserve做深度产品开发的开发者提供初始帮助。主要分为四个章节,首先介绍研究Rserve代码需要的基础知识和Rserve的常用名词,第二章介绍了Rserve的工作流程、QAP1消息协议和主要的Command,第三章分析了Rserve服务器端的代码结构和核心函数(内容有些冗长),最后简要罗列Rserve的Java/C++/PHP客户端。

简介

作为一个开放的数据分析工具软件,R提供了多种对外接口或扩展机制,1)针对算法扩展,提供了R package开发 (采用R / C/C++语言),2)针对批次执行或单次大计算量情形,提供了Batch命令行模式,3)对于应用整合,提供了C/Fortran API,方便第三方应用嵌入利用R的分析功能。第3)种模式不仅限定了编程语言,还要求应用开发人员处理R动态库初始化、内存处理、错误机制等技术细节(很容易出bug),针对这样的限制,一种Client-Server的框架还是很有必要的,消除了开发阶段的技术限制和门槛,在运行状态,规避了R动态库初始化、结束等处理的时间开销。RServe就是基于TCP/IP的R语言Client-Server框架的一种实现。

从2002年第一个版本发布,RServe目前已经演化到1.8-4。在RServe中,每个连接(Connection)有一个独立的工作空间(workspace)和工作目录,支持远程链接、安全认证、文件传输等功能。Rserve工具包也实现了Java、C++、PHP等客户端,方便其他编程语言与R数据结构的友好转换,通过SDK接口函数(或通信协议)将需要的数据加载到R,按照客户端指令进行相应的计算,并将结果返回到客户端,所有的数据和对象在连接期间一直是保持(persistent)的(在连接期间是有状态的)。下面的Java代码演示了调用本地Rserve服务,生成一个长度为10的正态分布数组。

RConnection c = new RConnection(); double d[] = c.eval("rnorm(10)").asDoubles();

如果把RServe作为一个工具,仅需了解如何利用RServe来开发第三方应用,可以看张丹的书或者RServe本身带的例子。但若要深度利用RServe,关心其运行机制与性能,则需要了解RServe的设计初衷及其设计机制,建议首先阅读RServe作者Simon

Urbanek在2003年一篇论文(时间久远了一些,但基本思想和技术路线没有变化),再根据需要补充一些前置知识,最后去研究其源代码。本文对RServe源代码进行了粗浅解读,为研究RServe代码提供一些初始帮助(RServe的使用方法本文不再赘述),也欢迎大家补充更正,一起加深理解,更好地利用RServe。 

RServe源代码阅读的难度主要来自于3个方面,1)RServe涉及R、动态链接库、网络编程、多线程等多方面的技术知识(以及不同操作系统下的差异细节),要求高;2)RServe的系统性的技术设计资料缺乏,除了Urbanek关于设计机制的论文外,很多技术细节散落在RServe官网的技术说明文档、RServe代码和RServe的版本演化说明文件;3)RServe源代码复杂,文件多,代码长(Rserv.c有5000余行代码),为处理不同操作系统和服务端口的差异,用了大量的宏(如#ifdef

unix),在抽象时还用了不少函数指针(源代码分析工具很难提供帮助)。为此,下面首先列举一下背景知识和RServe代码常见术语,然后介绍RServe的设计工作逻辑与对象,在此基础上,对服务端的核心源代码逐步分解,最后简要介绍一下客户端的封装逻辑

1 背景知识

  ◆ 1.1 研究Rserve代码的前置条件 

RServe其本质还是利用R本身的动态链接库的能力,外加Connection、Session、线程/进程的处理逻辑。因此,对RServe的深入了解前提基础是1)R内部的机制;2)网络及Socket编程,3)多线程编程。 

了解R的内置数据类型(SEXP),以及R动态链接库提供的一些基础函数(比如R_tryEval),大家可以阅读[R
 Internal](https://cran.r-project.org/doc/manuals/R-ints.html)、[R
External](https://cran.r-project.org/doc/manuals/R-exts.html)文档的相关章节。一个简单的测试条件就是能读懂如下程序:

#include <Rembedded.h>#include <Rdefines.h>static void doSplinesExample();int main(int argc, char *argv[]) {    Rf_initEmbeddedR(argc, argv);    doSplinesExample();    Rf_endEmbeddedR(0);    return 0; }static void doSplinesExample() {    SEXP e, result;    int errorOccurred;     // create and evaluate 'library(splines)'    PROTECT(e = lang2(install("library"), mkString("splines")));    R_tryEval(e, R_GlobalEnv, &errorOccurred);    if (errorOccurred) {        // handle error    }    UNPROTECT(1);     // 'options(FALSE)' ...    PROTECT(e = lang2(install("options"), ScalarLogical(0)));    // ... modified to 'options(example.ask=FALSE)' (this is obscure)    SET_TAG(CDR(e), install("example.ask"));    R_tryEval(e, R_GlobalEnv, NULL);    UNPROTECT(1);     // 'example("ns")'    PROTECT(e = lang2(install("example"), mkString("ns")));    R_tryEval(e, R_GlobalEnv, &errorOccurred);    UNPROTECT(1); }

对网络编程,特别是socket编程,包括客户端socket、服务器端socket、安全(如会话管理、SSLServerSocket)及非阻塞I/O机制(如缓冲区、通道、就绪选择)等。 

另外,建议熟悉一下unix下的fork()函数知识,了解进程创建过程、父子进程间关系等基础知识。

  ◆ 1.2 名词术语 

2 RServe的核心对象与逻辑

  ◆ 2.1 典型工作流程 

RServe典型的调用流程如下图所示:

RServe可以在R环境或命令行(R CMD Rserve)启动,附带对应的配置参数或配置文件(详情请参阅[RServe的说明文档](http://www.rforge.net/Rserve/doc.html))。Rserve代码中调用了R动态库,在RServe启动期间,初始化R环境。

当一个新的连接请求接受后,Rserve用fork()创建一个新的进程(这样每个连接是一个独立的数据空间),并在RServe工作目录(默认是/tmp/Rserv)下为该连接创建一个子工作目录(目录名字格式为conn*X*,其中*X*是连接唯一ID)。相对于Unix操作系统,Rserve在Windows操作系统下有一定的局限性。主要是因为Windows下没有类似fork的命令来快速复制创建一个新的RServe进程。Windows版本的RServe实例在一个时刻只支持一个连接,同一个RServe实例的所有连接有一个共同的工作目录。当然,在Windows下可以用外部的分布式计算框架,启动多个RServe实例来支持多个连接。

RServe完成连接的初始化后,将向客户端返回一个32字节的消息,描述了RServe的能力,每个属性4个字节,采用明文(不能用特殊字符)。消息的格式如下:

其他属性包括:

· "R151" - R版本号 (这里是 1.5.1)

· "ARpt" - authorization required(需要认证),如果第一个数据包不是CMD_login,连接将被关闭。这里,"pt"=plain text(明文), "uc"=unix crypt,  "m5"=MD5

· "K***" - 认证密码 (*** is the key)

· "TLS" - 切换到TLS

当连接关闭后,连接子工作目录如果为空,RServe将自动回收,但若非空,RServe将不会移除该目录,因为该目录中可能存放着其他本地应用(如Web服务器)需要访问的数据资源(比如图片),有本地应用负责该工作目录的移除工作。

  ◆ 2.2 通信协议

QAP是Rserve最初也是最根本的通信协议,下节将详细介绍。从版本1.7.0开始,Rserve还支持HTTP、HTTPS、WebSockets、TLS/QAP(并且支持通过CMD_switch从QAP到TLS/QAP的切换)。

Rserve HTTP与R内置http server功能类似,只不过通过fork()支持多个并行连接,并通过.http.request()来处理新的连接。

Websockets主要为HTML5网页浏览器与R建立一个长连接。它由2种协议模式,WebSocket(\*,"QAP") 对原始QAP做了websocket封装,要求浏览器客户端支持二进制的websockets协议(version
01或更高),数据传输效率高,可以通过ArrayBuffers直接进行GPU或CPU计算。WebSocket(*, "text")用明文方式。

另外,RServe允许HTTP server升级到同端口的Websockets(如果设置了http.upgrade.websockets enable)。

  ◆ 2.3 QAP1协议

为提升RServe与客户端间的数据传输效率,Rserve默认采用QAP1
(quad attributes protocol v1) 二进制信息数据协议(message oriented
protocol)。发起方(客户端)发出的请求(requeset)和服务器端的响应(response)都采用这种消息形式。每条消息由header
 part和data part组成 (data part可能为空)。

Header part由16个字节构成,数据结构如下表所示:

Data part包括了命令所需的额外参数信息。每个参数包括4字节的header:

· [0]  (byte) 参数类型

· [1]  (3 byte int) 参数长度

RServe支持的参数类型请参阅Rsrv.h,例如:

· DT_INT (4 bytes) integer

· DT_STRING (n bytes) null terminated string

· DT_BYTESTREAM (n bytes) any binary data

· DT_SEXP R's encoded SEXP

所有的int或double类型在消息传输中都采用Little

endian(低位在前,高位在后,Intel等处理器采用)形式,例如int=0x12345678将被表示为char[4]=(0x78,0x56,x34,0x12),转换函数/宏在Rsrv.h中定义。

Header part(16个字节)必须作为一个整体传输,而Data part可以根据需要切分到多个(packets),因此一条消息有 length+16个字节构成 (这里,length是data part的大小)。

  ◆ 2.4 命令(Command)

RServe支持的命令可以分为如下5类:

1. user authentication (用户认证)

2. evaluation of R expressions (R表达式评估)

3. assignment of values to R symbols (赋值)

4. file transfer (文件传输)

5. server shutdown (关闭RServe服务)

客户端Command具体包括([ ]表示参数是可选的):

CMD_RESP掩码(mask)为所有的响应消息预留。响应消息对应的command包括RESP_OK和RESP_ERR(24 bits)和状态码(8 bits)。

CMD_ctrlEval、CMD_ctrlSource这2个控制命令的执行结果(可能会改变一些变量的数值)将影响所有后续的其他客户端连接。另外,控制命令是异步执行的(返回状态RESP_OK仅仅表示成功进入执行队列,并不代表执行成功),当完成当前客户请求后才可能开始执行,在控制命令执行期间,新的客户连接将被放入队列。因此,控制命令最好是执行时间比较短,避免造成大量的客户请求被缓存。

OC模式是为了保证执行指令的安全而设立,表达式只在闭包(closure)内的环境执行。在此模式下,除了CMD_OCcall,其他命令都被禁止。在一个新连接建立后,服务端并不像普通模式下返回一个32字节的ID字符串,而是返回一个正常的QAP1消息(CMD_OCinit),消息至少有16个字节,保证客户端可以像普通ID字符串类似处理。每个CMD_OCcall是DT_SEXP(对LANGSXP编码)和一个OCref对象构成的闭包,在对表达式计算之前,Rserve需根据OCref对象进行表达式重构(de-reference)。

CMD_switch的作用是从QAP到TLS/QAP的切换。

作者:田春华 博士

昆仑数据首席数据科学家,2004年1月清华大学自动化系博士毕业。2004年-2015年在IBM中国研究院,负责数据挖掘算法研究和产品工作,在高端装备、产品运维服务、新能源运营优化等多领域,帮助中国、亚太、欧美领先企业,成功实施资产管理、运营优化、营销洞察等各类数据分析项目,为客户创造上亿美元收益。发表学术论文(长文)82篇(其中第一作者42篇),拥有36项专利申请(10项已授权)。研究兴趣:数据挖掘算法与应用。

昆仑数据K2Data
昆仑数据K2Data

专注工业大数据领域的洞察、技术、实战。相信知识的力量,让更多工业人,感受数据之美。

技术分析工业大数据技术源代码RServe
相关数据
IBM机构

是美国一家跨国科技公司及咨询公司,总部位于纽约州阿蒙克市。IBM主要客户是政府和企业。IBM生产并销售计算机硬件及软件,并且为系统架构和网络托管提供咨询服务。截止2013年,IBM已在全球拥有12个研究实验室和大量的软件开发基地。IBM虽然是一家商业公司,但在材料、化学、物理等科学领域却也有很高的成就,利用这些学术研究为基础,发明很多产品。比较有名的IBM发明的产品包括硬盘、自动柜员机、通用产品代码、SQL、关系数据库管理系统、DRAM及沃森。

https://www.ibm.com/us-en/
相关技术
数据分析技术

数据分析是一类统计方法,其主要特点是多维性和描述性。有些几何方法有助于揭示不同的数据之间存在的关系,并绘制出统计信息图,以更简洁的解释这些数据中包含的主要信息。其他一些用于收集数据,以便弄清哪些是同质的,从而更好地了解数据。 数据分析可以处理大量数据,并确定这些数据最有用的部分。

重构技术

代码重构(英语:Code refactoring)指对软件代码做任何更动以增加可读性或者简化结构而不影响输出结果。 软件重构需要借助工具完成,重构工具能够修改代码同时修改所有引用该代码的地方。在极限编程的方法学中,重构需要单元测试来支持。

参数技术

在数学和统计学裡,参数(英语:parameter)是使用通用变量来建立函数和变量之间关系(当这种关系很难用方程来阐述时)的一个数量。

数据科学技术

数据科学,又称资料科学,是一门利用数据学习知识的学科,其目标是通过从数据中提取出有价值的部分来生产数据产品。它结合了诸多领域中的理论和技术,包括应用数学、统计、模式识别、机器学习、数据可视化、数据仓库以及高性能计算。数据科学通过运用各种相关的数据来帮助非专业人士理解问题。

数据挖掘技术

数据挖掘(英语:data mining)是一个跨学科的计算机科学分支 它是用人工智能、机器学习、统计学和数据库的交叉方法在相對較大型的数据集中发现模式的计算过程。 数据挖掘过程的总体目标是从一个数据集中提取信息,并将其转换成可理解的结构,以进一步使用。

逻辑技术

人工智能领域用逻辑来理解智能推理问题;它可以提供用于分析编程语言的技术,也可用作分析、表征知识或编程的工具。目前人们常用的逻辑分支有命题逻辑(Propositional Logic )以及一阶逻辑(FOL)等谓词逻辑。

操作系统技术

操作系统(英语:operating system,缩写作 OS)是管理计算机硬件与软件资源的计算机程序,同时也是计算机系统的内核与基石。操作系统需要处理如管理与配置内存、决定系统资源供需的优先次序、控制输入与输出设备、操作网络与管理文件系统等基本事务。操作系统也提供一个让用户与系统交互的操作界面。

批次技术

模型训练的一次迭代(即一次梯度更新)中使用的样本集。

暂无评论
暂无评论~