学习总结

(1)“造火箭”:工业级推荐服务器的具体功能,以及实现工业级高并发推荐服务的主要机制。其中,推荐服务器的具体功能主要有:模型服务、数据库接口、推荐模块逻辑、补充业务逻辑等等,而工业级高并发推荐服务的主要机制有负载均衡、缓存和服务降级。

(2)“拧螺丝”:利用 Jetty 实践并搭建起了 SparrowRecSys 的推荐服务接口。这个过程中,注意每个注册到 Jetty Context 的 Servlet 服务中的主要业务逻辑,实际工作中是类似的。

(3)Jetty是一个提供HHTP服务器、HTTP客户端和javax.servlet容器的开源项目
作为一款嵌入式服务器框架,Jetty 的最大优势是除了 Java 环境外,不用配置任何其他环境,也不用安装额外的软件依赖,可以直接在 Java 程序中创建对外服务的 HTTP API,之后在 IDE 中运行或者打 Jar 包运行就可以了

知识点

关键描述

工业级推荐服务器的功能

主要包括模型服务、数据库接口、推荐模块逻辑、补充业务逻辑等

工业级高并发推荐服务的主要机制

负载均衡、缓存、降级机制

Jetty服务器的优势

轻量级嵌入式、Java开源社区完善、服务器开发效率和服务效率兼顾

Jetty实战

创建Jetty服务器和实现Jetty Servlet服务

文章目录

在一个实际的工业级推荐系统中,训练和实现推荐模型的工作量往往连一半都没有。大量的工作都发生在搭建并维护推荐服务器、模型服务模块,以及特征和模型参数数据库等线上服务部分。

同时,由于线上服务模块是直接服务用户,产生推荐结果的模块,如果一旦发生延迟增加甚至服务宕机的情况,就会产生公司级别的事故。因此毫不夸张地说,线上服务实际上是推荐系统中最关键的一个模块(王喆大佬说的)。

线上服务如果写得不好,不仅杂乱无章,而且难以升级维护。因此,为了掌握搭建起一个支持深度学习的、稳定可扩展的推荐服务的方法,本次task会学习线上服务器、特征存储、模型服务等模块的知识。先聚焦线上服务器,一起搭建直接产生推荐结果的服务接口。依次解决三大问题:

  • 一个工业级的推荐服务器内部究竟都做了哪些事情?
  • 像阿里、字节、腾讯这样级别的公司,它们的推荐系统是怎么承接住每秒百万甚至上千万的推荐请求的?
  • 我们自己该如何搭建一个工业级推荐服务器的雏形呢?

一、工业级推荐服务器的功能

【王喆-推荐系统】线上服务篇-(task1)线上高并发的推荐服务_负载均衡


图4 推荐系统技术架构示意图,源自王喆

上图是在第一章提到的架构图,其中模型部分内部长方形就是线上服务模块。线上服务模块的功能非常繁杂,它不仅需要跟离线训练好的模型打交道,把离线模型进行上线,在线进行模型服务(Model Serving),还需要跟数据库打交道,把候选物品和离线处理好的特征载入到服务器。

而且线上服务器内部的逻辑也十分地复杂,不仅包括了一些经典的过程,比如召回层和排序层,还包括一些业务逻辑,比如照顾推荐结果多样性,流行度的一些硬性的混合规则,甚至还包括了一些 AB 测试相关的测试代码。

二、高并发推荐服务的整体架构

大厂关于甚高并发具体的解决方案是集整个集团的技术精英打造的,而且维护一个高可用的服务集群的工作也不是一个算法工程师的主要工作方向。但还是需要能够从宏观的角度了解高并发的主要解决方案,因为它是一个工业级推荐系统的重要组成部分,也是我们在与架构组配合工作时应有的知识储备。

宏观来讲,高并发推荐服务的整体架构主要由三个重要机制支撑,它们分别是负载均衡、缓存、推荐服务降级机制。

2.1 负载均衡

负载均衡。它是整个推荐服务能够实现高可用、可扩展的基础。当推荐服务支持的业务量达到一定规模的时候,单独依靠一台服务器是不可行的,无论这台服务器的性能有多强大,都不可能独立支撑起高 QPS(Queries Per Second,每秒查询次数)的需求。

解决措施:
(1)增加服务器来分担独立节点的压力
(2)既然有多个劳动力在干活,所以还需要一个“工头”来分配任务,以达到按能力分配和高效率分配的目的,这个工头就是所谓的“负载均衡服务器”

下图中间的负载均衡服务器(Load Balancer)处在一个非常重要的位置。因此在实际工程中,负载均衡服务器也经常采用非常高效的 nginx 技术选型,甚至采用专门的硬件级负载均衡设备作为解决方案。

【王喆-推荐系统】线上服务篇-(task1)线上高并发的推荐服务_深度学习_02


图2 高并发情况下的负载均衡服务器(来源:GitHub)

2.2 缓存

“负载均衡”解决高并发的思路是“增加劳动力”,而如果从“减少劳动量”的角度来解决高并发带来的负载压力——我们可以利用缓存。

推荐过程特别是基于深度学习的推荐过程往往是比较复杂的,进一步来说,当候选物品规模比较大的时候,产生推荐列表的过程其实非常消耗计算资源,服务器的“劳动量”非常大。这个时候,我们就可以通过减少“硬算”推荐结果的次数来给推荐服务器减负。

比如说,当同一个用户多次请求同样的推荐服务时,我们就可以在第一次请求时把 TA 的推荐结果缓存起来,在后续请求时直接返回缓存中的结果就可以了,不用再通过复杂的推荐逻辑重新算一遍。再比如说,对于新用户来说,因为他们几乎没有行为历史的记录,所以我们可以先按照一些规则预先缓存好几类新用户的推荐列表,等遇到新用户的时候就直接返回。

因此,在一个成熟的工业级推荐系统中,合理的缓存策略甚至能够阻挡掉 90% 以上的推荐请求,大大减小推荐服务器的计算压力。

2.3 推荐服务降级机制

但不管再强大的服务集群,再有效的缓存方案,也都有可能遭遇特殊时刻的流量洪峰或者软硬件故障。在这种特殊情况下,为了防止推荐服务彻底熔断崩溃,甚至造成相关微服务依次崩溃的“雪崩效应”,我们就要在第一时间将问题控制在推荐服务内部,而应对的最好机制就是“服务降级”。

“服务降级”:抛弃原本的复杂逻辑,采用最保险、最简单、最不消耗资源的降级服务来渡过特殊时期。比如对于推荐服务来说,我们可以抛弃原本的复杂推荐模型,采用基于规则的推荐方法来生成推荐列表,甚至直接在缓存或者内存中提前准备好应对故障时的默认推荐列表,做到“0”计算产出服务结果,这些都是服务降级的可行策略。

总之,“负载均衡”提升服务能力,“缓存”降低服务压力,“服务降级”机制保证故障时刻的服务不崩溃,压力不传导,这三点可以看成是一个成熟稳定的高并发推荐服务的基石。

三、搭建一个工业级推荐服务器的雏形

在 Sparrow Recsys 里面实践一下搭建推荐服务器的过程,看看如何一步步拧螺丝,搭建起一个可用的推荐服务器。当然,它肯定无法直接具备负载均衡这些企业级服务的能力,但它可以作为一个工业级推荐服务器的雏形。以此为起点,逐渐把它扩展成为一个成熟的推荐服务。

3.1 Jetty服务器框架

首先,我们要做的就是选择服务器框架。这里,我们选择的服务器框架是 Java 嵌入式服务器 Jetty。为什么我们不选择其他的服务器呢?原因有三个。

  • 第一,相比于 Python 服务器的效率问题,以及 C++ 服务器的开发维护难度,Java 服务器在效率和开发难度上做到了一个权衡,而且互联网上有大量开源 Java 项目可以供我们直接融合调用,所以 Java 服务器开发的扩展性比较好。
  • 第二,相比 Tomcat 等其他 Java 服务器,Jetty 是嵌入式的,它更轻量级,没有过多 J2EE 的冗余功能,可以专注于建立高效的 API 推荐服务。而 Tomcat 更适用于搭建一整套的 J2EE 项目。
  • 第三,相比于基于 Node.js、Go 这样的服务器,Java 社区更成熟和主流一些,应用范围更广。

每一种技术选择都有它的优势,C++ 的效率更高,Python 更便捷,Go 的上升势头也愈发明显,我们只要清楚 Jetty 是企业级服务的选择之一就够了。

Jetty是一个提供HHTP服务器、HTTP客户端和javax.servlet容器的开源项目
作为一款嵌入式服务器框架,Jetty 的最大优势是除了 Java 环境外,你不用配置任何其他环境,也不用安装额外的软件依赖,你可以直接在 Java 程序中创建对外服务的 HTTP API,之后在 IDE 中运行或者打 Jar 包运行就可以了

下面就是 Sparrow Recsys 中创建推荐服务器的代码。

public class RecSysServer {
//主函数,创建推荐服务器并运行
public static void main(String[] args) throws Exception {
new RecSysServer().run();
}
//推荐服务器的默认服务端口6010
private static final int DEFAULT_PORT = 6010;


//运行推荐服务器的函数
public void run() throws Exception{
int port = DEFAULT_PORT;
//绑定IP地址和端口,0.0.0.0代表本地运行
InetSocketAddress inetAddress = new InetSocketAddress("0.0.0.0", port);
//创建Jetty服务器
Server server = new Server(inetAddress);
//创建Jetty服务器的环境handler
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
context.setWelcomeFiles(new String[] { "index.html" });


//添加API,getMovie,获取电影相关数据
context.addServlet(new ServletHolder(new MovieService()), "/getmovie");
//添加API,getuser,获取用户相关数据
context.addServlet(new ServletHolder(new UserService()), "/getuser");
//添加API,getsimilarmovie,获取相似电影推荐
context.addServlet(new ServletHolder(new SimilarMovieService()), "/getsimilarmovie");
//添加API,getrecommendation,获取各类电影推荐
context.addServlet(new ServletHolder(new RecommendationService()), "/getrecommendation");
//设置Jetty的环境handler
server.setHandler(context);


//启动Jetty服务器
server.start();
server.join();
}

3.2 Servlet 服务

上面创建 Jetty 服务的过程简单直观,搭建起一套推荐服务。推荐服务的主要业务逻辑并不在这里,而是在每个注册到 Jetty Context 中的 Servlet 服务中。下面使用其中最简单的 Servlet 服务 MovieService,Jetty 中的 Servlet 服务这样写:

//MovieService需要继承Jetty的HttpServlet
public class MovieService extends HttpServlet {
//实现servlet中的get method
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException {
try {
//该接口返回json对象,所以设置json类型
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
response.setCharacterEncoding("UTF-8");
response.setHeader("Access-Control-Allow-Origin", "*");

//获得请求中的id参数,转换为movie id
String movieId = request.getParameter("id");
//从数据库中获取该movie的数据对象
Movie movie = DataManager.getInstance().getMovieById(Integer.parseInt(movieId));


if (null != movie) {
//使用fasterxml.jackson库把movie对象转换成json对象
ObjectMapper mapper = new ObjectMapper();
String jsonMovie = mapper.writeValueAsString(movie);
//返回json对象
response.getWriter().println(jsonMovie);
}else {
response.getWriter().println("");
}


} catch (Exception e) {
e.printStackTrace();
response.getWriter().println("");
}
}

唯一的不同就是其中的业务逻辑。如果你已经从 GitHub 上下载了 Sparrow Recsys 项目把它运行起来,并且在浏览器中输入 http://localhost:6010/getmovie?id=1,就可以看到 getMovie 接口的返回对象了:

{"movieId":1,
"title":"Toy Story",
"releaseYear":1995,
"imdbId":"0114709",
"tmdbId":"862",
"genres":["Adventure","Animation","Children","Comedy","Fantasy"],"ratingNumber":10759,"averageRating":3.9149549214611024,
"topRatings":[{"rating":
{"movieId":1,"userId":136,"score":5.0,"timestamp":1415635425}},{"rating":
{"movieId":1,"userId":124,"score":5.0,"timestamp":1134475984}},{"rating":
{"movieId":1,"userId":93,"score":5.0,"timestamp":835037871}},{"rating":
{"movieId":1,"userId":84,"score":5.0,"timestamp":832543433}},{"rating":
{"movieId":1,"userId":82,"score":5.0,"timestamp":1317331523}},{"rating":
{"movieId":1,"userId":58,"score":5.0,"timestamp":1144058408}},{"rating":
{"movieId":1,"userId":39,"score":5.0,"timestamp":859325696}},{"rating":
{"movieId":1,"userId":34,"score":5.0,"timestamp":846509445}},{"rating":
{"movieId":1,"userId":19,"score":5.0,"timestamp":855176628}},{"rating":
{"movieId":1,"userId":6,"score":5.0,"timestamp":858275452}}]}

四、作业

4.1 第一题

在一个高并发的推荐服务集群中,负载均衡服务器的作用至关重要,如果你是负载均衡服务器的策略设计师的话,你会怎么实现这个“工头”的调度策略,让它能够公平又高效的完成调度任务呢?(比如是按每个节点的能力分配?还是按照请求本身的什么特点来分配?如何知道什么时候应该扩展节点,什么时候应该关闭节点?)

答:源地址哈希,或根据服务器计算能力加权随机分配。
(1)当出现大量节点利用率很低时,进行资源回收,减少虚拟机;
(2)当大部分节点都出现overload情况,进行扩容,增加虚拟机数量。

常用的负载均衡的策略有三种,分别是轮询调度、哈希调度和一致性哈希调度

  • 轮询调度就是以轮询的方式依次把请求调度到不同的服务器。在服务器的算力等硬件配置不同的时候,我们还可以为每个服务器设定权重,按权重比例为能力强的服务器分配更多的请求。
  • 哈希调度指的是通过某个哈希函数把 key 分配给某个桶,这里 key 可以是请求中的用户 ID,物品 ID 等 ID 型信息,桶的总数就是服务器的总数。这样一来,我们就可以把某个用户的请求分配给某个服务器处理。这么做的好处是可以让一个 key 落在固定的服务器节点上,有利于节约服务器内部缓存的使用。

哈希方式的缺点在于无法高效处理故障点,一旦某个点有故障需要减少桶的数量,或者在 QPS 增大时需要增加服务器,整个分配过程就被完全打乱。因此,一致性哈希调度就是更好的解决方案,简单来说就是使用哈希环来解决计算节点的增加和减少的问题

4.2 第二题

(2)在一个成熟的工业级推荐系统中,每个用户请求的时间、地点、context 都不一样,缓存策略是怎么工作的,才能把这些数据大部分都缓存起来?

如果请求中的变量每次都不一样,那我们确实就没有必要进行缓存了,因为每次返回的结果都是不同的。但真实情况往往不是这样,我们其实可以在具体的业务场景中挖掘出巨大的优化空间。

1)比如,电商网站存在着大量没有购买记录的新用户,我们其实可以根据这些新用户有限的特征把他们分成少量的几个类别,对一个类别内的用户展示同样的推荐结果。这样,我们就没必要每次都请求复杂的推荐模型了。

2)再比如,同一个用户有可能多次请求同一个页面,如果推荐系统对这些操作进行了缓存,就不用对每次重复的请求重复计算推荐结果了,在处理完首次请求之后,面对之后的重复请求,推荐系统直接返回缓存结果就可以了。当然,推荐系统具体能存储多少用户缓存,也取决于硬件配置,也取决于缓存的过期时间,这些都需要我们灵活进行配置。

4.3 第三题

推荐系统中的冷启动策略指的是什么?

【答】它是指推荐系统在没有可用信息,或者可用信息很少的情形下怎么做推荐的问题,冷启动可以分为用户冷启动和物品冷启动两类。

  • 用户冷启动:没有可用的用户历史行为信息以进行推荐。
    利用其他用户特征:比如注册时的信息,访问 APP 时可以获得的地点、时间信息等等,根据这些有限的信息,可以为用户做一个聚类,为每类冷启动用户返回合适的推荐列表。也可以利用可用的冷启动特征,来构建一个较简单的冷启动推荐模型。
  • 物品冷启动:新加入系统的物品,它们没有跟用户的交互信息。
    (1)用类似用户冷启动的方式解决,
    (2)还可以通过物品分类等信息找到一些相似物品,如果这些相似物品已经具有了预训练的 Embedding,
    (3)也可以采用相似物品 Embedding 平均的方式,来快速确定冷启动物品的 Embedding,让它们通过 Embedding 的方式参与推荐过程。

Reference

(1)https://github.com/wzhe06/Reco-papers
(2)《深度学习推荐系统实战》,王喆
(3)​​​Jetty官方文档翻译​​​ (4)Jetty官方文档:http://www.eclipse.org/jetty/documentation/
(5)​​一致性哈希算法的理解与实践​