作者:wbrecom
1 背景
在微博推荐的体系架构中,计算层是一个中间层,主要承担排序的工作,是整体架构的重要的组成部分。计算层的核心是lab_common_so框架,它是一个C/C++语言编写的高效计算服务。
微博推荐在早期的发展过程中,承接了大量的业务,在这段时期内,根据需求不断总结和归纳,诞生了基于apache + mod_python的common_recom_framework(以下简称CRF),该框架具有如下特性:
- 透明化的数据获取:使用统一的函数获取数据,而不用关心数据的存储格式;
- 统一的接口形态:所有业务的对外接口,形式上保持一致;
- 规范化的业务流程:业务只需要按照固定的模板编写即可,开发极为迅速;
- 插件式的业务开发:业务的增加,修改都是热插拔式,互不影响。
CRF的诞生,使得开发人员,不必为多个业务维护多套代码,降低了维护成本。同时,开发人员只需要在框架之上做一些二次开发工作,极大地提高了开发效率。
随着微博推荐业务的高速发展,业务的访问量越来越大,业务逻辑变得越复杂,比如CTR预估模型的引入。我们发现CRF已经有点力不从心了,主要体现在以下三个方面:
- CTR预估模型会耗费较大的计算量,是CPU密集型的,而python在这方面的处理效率不那么让人满意,耗时很难降低;
- 预估模型整体的逻辑较为复杂,使得代码比较臃肿,业务迭代效率大大降低;
- apache是以阻塞式的多进程的方式来工作的,当套接字的I/O阻塞时,进程会挂起,一旦连接数增加,apache不得不生成更多的进程来响应请求。而更多的进程,意味着进程间切换的开销增加,也就是说,当业务访问量较大时,apache处理不过来。
为解决上述问题,我们决定把CPU密集型的复杂计算抽取出来,使用高效的C++服务来完成,这样就减轻了CRF的计算压力。同时,这种做法也可以让CRF更加专注于策略,代码看起来更加清爽,迭代速度会更快,能够解决问题(1)和问题(2)。于是,lab_common_so框架应运而生。
对于问题(3),经过一段时间的探索,我们演化出了recom_unite_front(以下简称RUF)框架。这是一个基于openresty开发的二次框架,它比CRF更为轻量,使用了异步非阻塞的事件模型,很好的解决了问题(3)。
下图 1展示了CRF框架的发展。可以看到,CRF向上轻量化为RUF,具有更快的迭代速度,更强的并行处理的能力。CRF向下稳定化为lab_common_so,具有更高的计算效率,用于处理复杂的业务逻辑。
lab_common_so是CRF的发展和延续,因此它从一开始,就天然的具有CRF的几个特性,并在其基础之上,新增了如下特性:
- 数据本地化:一些小规模的数据,可以加载到共享内存中,减少访问网络数据的开销;
- 非中断式的资源更新:共享内存中的数据,可以实时更新,而不影响业务;
- 可配置的算法支持:算法的调用,可通过配置文件来指定,避免修改代码;
基于以上特性,lab_common_so的可以用于但不限于如下场景:
- 高效的实时在线计算服务
- 并发式的离线计算服务
- 数据存储代理算法模型训练
接下来的章节,将详细剖析lab_common_so的框架。
2 框架整体设计
本节介绍lab_common_so的整体设计以及框架处理流程。
2.1 框架整体构成
lab_common_so框架整体可以使用图 2来描述。
关于各部分的设计细节,由后续章节“3 框架类设计”叙述。
2.2 服务启动
图 3展示了lab_common_so的启动流程。
程序首先读入一个配置文件,确定服务运行所需要的基本参数,包括日志级别,线程数目,套接字的相关属性等。
然后程序根据读入的基本参数,开始执行初始化。这个过程主要是将一些本地数据,加载到内存中。
第三步,创建指定数目的线程,并对每个线程进行初始化。线程的初始化工作包括db_company和work_company。
最后,绑定指定的服务端口,开始监听。服务的端口有两个:一个是查询端口,服务通过监听这个端口,来响应各业务请求。一个是控制端口,服务通过监听这个端口,来响应命令类的请求,比如更新共享数据,更新业务等。
当服务端接收到查询请求之后,开始处理。图 4展示了查询服务的处理流程。
第一步:服务端接受到请求。请求使用的是woo协议,这是我们内部研发的一个协议,见“3.1 woo协议”。
第二步:服务的某一个空闲线程,对接受到的请求进行处理。线程使用work_company对象的work_core函数,解析出请求中的参数。
第三步:根据上个步骤提取的参数,使用work_interface_factory对象,获取业务处理类,然后调用业务处理类的work_core函数进行处理。
第四步:在业务处理类中,通过db_interface获取远程数据,通过global_db_interface获取本地数据,通过algorithm_core函数执行算法。
第五步:将执行完的结果,拼装成接口指定格式(一般使用json)返回。
类似地,当服务端接收到控制请求后,开始处理。
图 5以更新全局数据为例,展示了控制服务的处理流程。
第一步:服务端接受woo协议的请求。
第二步:服务端解析请求,以确定该如何响应。这里响应的是更新全局数据的命令。
第三步:服务端会新建一个global_db_company,并将新的数据加载入内存。
第四步:对于每个线程,使用新的global_db_company去初始化。
第五步:当所有线程都更新完毕后,交换新旧指针。
第六步,睡眠一段时间,以确保旧的资源无人使用。然后释放旧指针指向的内存。
3 框架类设计
这一节将详细讲述lab_common_so框架的类设计。框架的UML类图,如图 6所示。
对于每个线程,包含一个WorkCompany类对象的指针和一个DbCompany类对象的指针,前者用于管理业务资源,后者用于管理数据资源。
因此,我们将可以简单的将所有的类分为两种类别:一种是业务类,包括“2.1 框架整体构成”中的业务部分和算法部分;一种是数据类,包括“2.1 框架整体构成”中的全局数据部分和远程数据部分。
接下来,将对业务和算法这两个类别,分述各类的设计。
业务类主要包含四个类,分别是WorkCompany,WorkInterfaceFactory,WorkInterface和AlgorithmInterface。分述如下:
3.1.1 WorkCompany
WorkCompany类中包含了WorkIntefaceFactory类对象的指针和work_core函数。这个类是业务类的最高层。
当服务的某个线程接受到请求以后,首先调用WorkCompany类的work_core函数,对传入的请求参数进行解析,然后调用WorkCompany类的WorkIntefaceFactory类对象的指针,获取指定的WorkInteface类对象进行业务处理。
3.1.2 WorkInterfaceFactory
顾名思义,这是一个工厂类,用于对多个WorkInterface进行管理。
该类包含的map,记录了业务名称与业务处理类的对应关系,这样,我们就可以通过传入参数中的指定字段,调用get_interface函数,获取对应的业务处理类了。
业务名称与业务处理类的对应关系,是通过配置文件work_config.ini指定的。当需要新增业务时,我们无需更新框架代码,而是更新配置文件的即可。
3.1.3 WorkInterface
这是每个业务处理类的抽象基类。所有的业务处理类,都需要继承该类,并实现该类的work_core函数。work_core函数是所有业务处理类的入口函数。
此外,在WorkInterface中,还维护了一个VEC_PAIR_MAP_ALG结构,记录了每个算法处理类与算法名称的对应关系,这一对应关系,也是通过配置文件指定的。在业务处理类中,可以根据这一对应关系,调用指定的算法处理函数。
3.1.4 AlgorithmInterface
这是每个算法处理类的抽象基类。所有的算法处理类,都需要继承该类,并实现该类的algorithm_core函数。其设计和WorkInterface相似。
3.2 数据类设计
在lab_common_so中,db_company管理了两类数据分。一类是静态全局数据,这类数据被加载在共享内存中,所有的线程共享,其相关类包括GlobalDbCompany,GlobalDbInterfaceFactory和GlobalDbInterface。另一类是远程数据,支持多种数据库,比如redis,memcache等,其相关类包括DbInterfaceFactory和DbInterface。
3.2.1 db_company
每个db_company的对象,包含了一个global_db_company,同时还记录了db_interface映射关系。
其中global_db_company管理了全局数据资源,db_interface映射关系记录了db_id以及db_name和db_interface的对应关系,在访问数据时,根据配置文件中的DB_ID字段或者DB_NAME字段,就可以获取对应的db_interface类对象的指针。
3.2.2 GlobalDbCompany
该类包含了一个map,记录了全局数据库名称和全局数据库的对应关系,这个对应关系,同样是通过配置文件指定的。load_config函数的作用即是读取配置文件,生成对应关系。给定指定的数据库名称,就可通过get_global_db_interface获取全局数据库对象。
update_global_db_interface函数则是用于响应全局数据更新命令。
3.2.3 GlobalDbInterfaceFactory
该类只包含了一个函数,get_global_db_interface函数。这个函数和GlobalDbCompany中的同名函数的不同之处在于,该函数是根据指定的数据库类型,构建全局数据库对象。因此,该函数仅在GlobalDbCompany中的load_config函数中被调用。
3.2.4 GlobalDbInterface
该类是每个全局数据类的抽象基类。在此基础上,可以派生出多种数据类型。
is_exist函数用于确定指定数据是否在数据库中,load_db_config函数用于将读取静态数据,载入到内存中。
3.2.5 DbInterfaceFactory
该类的设计和GlobalDbInterfaceFactory一样。
3.2.6 DbInterface
该类是远程数据的抽象基类。在此基础上,可以派生出多种数据存储格式。
很多远程数据库如redis、memcache等,都支持批量获取多个key的value值,因此我们将mget函数设计成为纯虚函数,必须实现。
3.3 总结
可以看到,无论是业务类还是算法类,无论是全局数据类还是远程数据类,我们的设计思路都是一致,都采用了工厂模式。
我们通过继承的方式提供了统一的接口,这样在使用之时,对数据的访问是透明化的。
我们通过工厂模式,隐藏了类的创建细节,从而使得程序具有更高的扩展性。
4 框架基础
本节将描述在lab_common_so框架中的一些基础的定义。
4.1 woo协议
woo是一个轻型通讯框架,其通讯协议及日志系统比较完善。
woo的服务端由C/C++语言编写,客户端则提供了基于C/C++,PHP以及Python等多种语言版本。
woo协议采用了基于epoll的I/O通信模型,具有较好的I/O处理能力。还提供了对多线程的支持。
由于协议结构简单,通信轻量化,在微博推荐内部,使用的较为广泛。
4.2 GlobalDb类型
该类型用于支持全局静态数据的载入及获取。数据多以文件形式挂在本地磁盘,在框架启动时加载入内存,全局唯一,所有线程共享。
GlobalDbInterface是所有全局静态数据读取类的基类,提供了一个is_exist通用函数接口用于查询指定key是否在数据库中。该类的某些派生类还提供了get_value和mget_value等特定函数接口。
目前支持如下类型:
__gnu_cxx::hash_set
__gnu_cxx::hash_map<uint64_t, uint32_t=””>
MapDb(自研)
4.3 Db类型
该类型用于支持远程数据的获取。
DbInterface是所有远程数据读取类的基类,目前派生出了四种类型:
- redis:提供对远程redis数据库的访问
- woo:提供对基于woo协议服务的访问
- openAPI:提供对http服务的访问
- MC:提供对远程memcache数据库的访问
目前提供了四个通用函数接口:
- get(uint64_t n_key):根据整型key访问数据库
- s_get(char* p_str_key):根据字符串key访问数据库
- mget(uint64_t n_keys[]):根据多个整型key访问数据库
- s_mget(char* p_str_keys[]):根据多个字符串key访问数据库
此外,还提供了一个get_multi_db函数,可对多种数据请求,并行访问不同数据库。
4.4 Work类型
该类型是业务类,提供给用户进行二次开发。
框架对业务执行了so化,使得业务可以快速上线迭代,并能让服务支持多个业务。
我们定义了统一的接口格式,在基类work_interface中提供了统一的接口函数work_core。
对于输入的请求串,必须是json格式的,例如{“api”:”example”, “cmd”:”query”, “body”:”welcome!”}。其中api是必需的,以此来获取指定的业务,并将请求中的其它参数传递给业务。对于输出格式,没有进行限制。
4.5 Algorithm类型
该类型是算法类,提供给用户进行二次开发。
框架对算法执行了so化,使得算法库可以迅速上线,并能让多个业务使用多个算法库。
我们定义了统一的接口格式,在基类algorithm_interface中提供了统一的接口函数algorithm_core。
4.6 服务类型
框架可编译成两个可执行文件,分别为lab_common_main和lab_common_svr。
lab_common_main程序是离线处理程序,执行完请求之后会退出。
lab_common_svr程序是在线服务程序,持续监听端口,接收请求并处理。
目前在线程序提供两种服务,一种是查询服务,一种是控制服务。
5 lab_common_so框架的演进
早期的计算层框架,名为lab_common,是一个具有实验性质的通用框架,它是lab_common_so框架的雏形。我们首先使用这个框架开发了几个业务,性能超出预期。
在lab_common框架的使用过程中,我们发现这个框架有一个很大的缺点:业务逻辑的每一次变动,都需要重新编译代码,生成新的服务程序。发布到线上则需要重启服务,期间所有的业务都会暂停。也就是说,lab_common框架并没有将业务做到热插拔,实际还是损失了CRF的特性。尽管计算层的业务趋于稳定,但是每一次改动都会导致服务中断,这是不可接受的。
于是,我们对框架进行了升级。我们将每一个业务代码,都编译成so文件,框架通过读取配置文件,打开指定业务的so文件。当我们修改一个业务,或者新增一个业务时,只需要单独编译业务代码,将生成的新的so文件,发送到线上,然后发送更新配置文件的命令,即可实现动态加载。在整个更新过程中,服务无需终止,其它业务不受影响,可以继续对外提供服务。这样,我们就成功的使框架具备了热插拔的特性。因为我们将业务so化了,所以,我们将框架的名称修改为lab_common_so。
之后,lab_common_so框架由于其易用性与稳定性,开始在团队内部被广泛使用。随着越来越多的业务使用该框架开发,我们逐渐发现了一些功能及细节上的问题。对此,我们一一做了扩充支持与修复,比如支持多线程获取数据,支持多个算法调用,支持多种格式的数据获取等等。
如今,已有十多个使用lab_common_so框架开发的业务,在线上稳定运行,每天流量数以亿计。
从最早的lab_common框架诞生,到当前lab_common_so框架在线上稳定运行,已经过去了一年半的时间。微博推荐团队的各个成员,都参与到了这个框架的设计与演进中。因此,当前的lab_common_so框架,是大家群策群力的结果。
当然,框架本身还有不足,比如:
(1)db_interface类可以写的更抽象一些;
(2)HTTP访问目前仅支持GET方式;
(3)db_interface类也可以像work_interface类一样so化
(4)多线程获取数据有一定的局限性
这些都是以后框架演进过程中需要解决的问题。
6 总结
本篇文章简单地介绍了lab_common_so框架,主要从框架的起源,发展,设计方面进行描述,力图让读者对本框架整体有一个初步的了解。
lab_common_so框架已经开源,具体的实现细节,可以从https://github.com/wbrecom/lab_common_so上获取。开源项目中也包含了一份较为详细的框架的使用说明文档。
End.
转载请注明来自36大数据(36dsj.com):36大数据 » 微博推荐计算层解决方案:lab_common_so框架
爱盈利-运营小咖秀 始终坚持研究分享移动互联网App数据运营推广经验、策略、全案、渠道等纯干货知识内容;是广大App运营从业者的知识启蒙、成长指导、进阶学习的集聚平台;