Flask首页新闻推荐与RPC
-
RPC应用场景
RPC更多的面向产品内部服务器间的通讯,目的时高效,不注重通用。
-
选用RPC原因
推荐系统: 作用:根据不同的用户id,经过计算,返回推荐的文章id 现在需要解决的问题是: web应用程序如何跟推荐系统通讯。 方案一: 把推荐系统封装成python语言能直接调用的类或者函数,直接调用。 缺点:耦合性高,维护困难 方案二: 使用Flask或者其他web框架,把推荐系统编成web应用,通过接口调用。 优点:通用,http协议标准的协议 缺点:效率低,成本高 方案三: 使用rpc调用 优点:效率高,采用二进制数据直接传递(相对于http),耦合低(相对于封装成函数或者类) 缺点:不通用,需要专门的客户端与服务器能够支持特定的二进制协议
-
使用Protobuf 定义的接口
新建reco.proto文件
syntax = "proto3"; message UserRequest { string user_id=1; int32 channel_id=2; int32 article_num=3; int64 time_stamp=4; } message Track { string click=1; string collect=2; string share=3; string read=4; } message Article { int64 article_id=1; Track track=2; } message ArticleResponse { string exposure=1; int64 time_stamp=2; repeated Article recommends=3; } service UserRecommend { rpc user_recommend(UserRequest) returns(ArticleResponse) {} }
-
代码生成
编译生成代码
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. reco.proto
-I
表示搜索proto文件中被导入文件的目录--python_out
表示保存生成Python文件的目录,生成的文件中包含接口定义中的数据类型--grpc_python_out
表示保存生成Python文件的目录,生成的文件中包含接口定义中的服务类型
在rpc目录下执行上述命令,会自动生成如下两个rpc调用辅助代码模块:
-
reco_pb2.py 保存根据接口定义文件中的数据类型生成的python类
-
reco_pb2_grpc.py 保存根据接口定义文件中的服务方法类型生成的python调用RPC方法
-
服务端
import reco_pb2 import reco_pb2_grpc import grpc from concurrent.futures import ThreadPoolExecutor import time # rpc接口定义中服务对应成Python的类 class UserRecommendService(reco_pb2_grpc.UserRecommendServicer): # 在接口定义的同名方法中补全,被调用时应该执行的逻辑 def user_recommend(self, request, context): # request是调用的请求数据对象 user_id = request.user_id channel_id = request.channel_id article_num = request.article_num time_stamp = request.time_stamp response = reco_pb2.ArticleResponse() response.exposure = 'exposure param' response.time_stamp = round(time.time()*1000) recommends = [] for i in range(article_num): article = reco_pb2.Article() article.track.click = 'click param {}'.format(i+1) article.track.collect = 'collect param {}'.format(i+1) article.track.share = 'share param {}'.format(i+1) article.track.read = 'read param {}'.format(i+1) article.article_id = i+1 recommends.append(article) response.recommends.extend(recommends) # 最终要返回一个调用结果 return response def serve(): """ rpc服务端启动方法 """ # 创建一个rpc服务器 server = grpc.server(ThreadPoolExecutor(max_workers=10)) # 向服务器中添加被调用的服务方法 reco_pb2_grpc.add_UserRecommendServicer_to_server(UserRecommendService(), server) # 微服务器绑定ip地址和端口 server.add_insecure_port('127.0.0.1:8888') # 启动rpc服务 server.start() # start()不会阻塞,此处需要加上循环睡眠 防止程序退出 while True: time.sleep(10) if __name__ == '__main__': serve()
-
客户端
import grpc import reco_pb2 import reco_pb2_grpc import time def feed_articles(stub): # 构建rpc调用的调用参数 user_request = reco_pb2.UserRequest() user_request.user_id = '1' user_request.channel_id = 1 user_request.article_num = 10 user_request.time_stamp = round(time.time()*1000) # 通过stub进行方法调用,并接收调用返回值 ret = stub.user_recommend(user_request) print('ret={}'.format(ret)) def run(): """ rpc客户端调用的方法 """ # 使用with语句连接rpc服务器 with grpc.insecure_channel('127.0.0.1:8888') as channel: # 创建调用rpc远端服务的辅助对象stub stub = reco_pb2_grpc.UserRecommendStub(channel) # 通过stub进行rpc调用 feed_articles(stub) if __name__ == '__main__': run()
-
首页新闻推荐接口编写
from rpc import reco_pb2, reco_pb2_grpc class ArticleListResource(Resource): """ 获取推荐文章列表数据 """ def _feed_articles(self, channel_id, timestamp, feed_count): """ 获取推荐文章 :param channel_id: 频道id :param feed_count: 推荐数量 :param timestamp: 时间戳 :return: [{article_id, trace_params}, ...], timestamp """ # 组装调用gRPC方法的参数 user_request = reco_pb2.UserRequest() user_request.user_id = g.user_id or 'annoy' user_request.channel_id = channel_id user_request.article_num = feed_count user_request.time_stamp = round(time.time() * 1000) # 使用跟推荐系统通讯的通道,创建存根。 stub = reco_pb2_grpc.UserRecommendStub(current_app.rpc_reco) ret = stub.user_recommend(user_request) return ret.recommends, ret.time_stamp def get(self): """ 获取文章列表 """ qs_parser = RequestParser() qs_parser.add_argument('channel_id', type=parser.channel_id, required=True, location='args') qs_parser.add_argument('timestamp', type=inputs.positive, required=True, location='args') args = qs_parser.parse_args() channel_id = args.channel_id timestamp = args.timestamp per_page = constants.DEFAULT_ARTICLE_PER_PAGE_MIN try: feed_time = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(time.time())) except Exception: return {'message': 'timestamp param error'}, 400 results = [] # 获取推荐文章列表 feeds, pre_timestamp = self._feed_articles(channel_id, timestamp, per_page) # 查询文章 for feed in feeds: article = cache_article.ArticleInfoCache(feed.article_id).get() if article: article['pubdate'] = feed_time article['trace'] = { 'click': feed.track.click, 'collect': feed.track.collect, 'share': feed.track.share, 'read': feed.track.read } results.append(article) return {'pre_timestamp': pre_timestamp, 'results': results}