搜索返回的多条结果中,包括自天然搜索结果和特型搜索结果。
天然搜索结果是针对用户请求(query),返回互联网上相关性较高的内容。是否知足用户需求取决于互联网内容及相关性算法。
特型搜索则是针对query,线下先对内容进行挖掘、整合(线下的方式有多种,其中知识图谱 是一个方向),线上直接返回知足用户需求的结果。好比当搜索"冰与火之歌"时,排在前面的百科、小说、影视等部分就属于特型搜索结果。算法
除了和传统搜索同样有着严格的性能要求外,特型搜索服务还有两个比较大的特色:
1. 开放:一次用户请求的结果是由多个不一样的服务来完成的,并且不一样的服务每每由不一样的小组负责研究。
2. 收敛:若是一次请求返回了多个特型结果,须要对结果进行聚合再返回。
因此特型搜索服务模型就是在叶子节点开放、非叶子结点收敛的一种树形搜索结构:缓存
针对该服务模型,极端一点的作法可让各个service自由发挥,而后经过rpc进行访问就能够了。但这样作显然是比较浪费和容易引发混乱的作法:
1. 接口标准:首先是接口问题,上游服务对下游服务进行进行聚合的时候,若是不能识别对方的返回结果,那"后来人"只能骂娘了。
2. 流量分发:下游服务虽然能够独立开发,但仍须要向上游服务申请流量,也就是流量分发的功能,这实际上是能够统一处理的。
3. 服务调用:若是service之间都是经过rpc调用,当调用层次比较深的时候,在对性能要求十分严格的环境下,是不可能知足要求的。
4. 组件开发:一些通用的组件,好比日志、缓存、内存词典等,若是让每一个服务都去开发,是很是浪费的。
如下对"接口标准", "流量分发", "服务调用"展开说明一下。架构
接口标准是为了下降服务调用和聚合的成本。
服务的接口是比较简单的,用protobuf service的描述相似:函数
service ServiceAPI { rpc query(message.Request) returns (message.Response); };
其中Request表示用户的一次请求信息,对各服务都是同样的。
Response稍微复杂一些,由于每一个service的结果是不同的,必须提供扩展机制。固然用protobuf是比较容易作到的:性能
message Response { //common fields: ... ... repeated Result = 10; }; message Result { //common fields: ... ... extensions 128 to 512; };
固然也能够不用extensions, 而是在Result里面加入自定义Message,这样能够在Result里面看到全部的message(若是某些因素(好比历史遗留问题)要求你这么作的话),咱们采用的就是后者。spa
流量分发是将用户请求发送到服务的过程。要解决几个问题:架构设计
若是是转发,怎么知道转发给谁?是全量转发仍是部分转发?
转发实际上是AggregatedService的职责,咱们的转发机制也是集成在这个基类里面的。主要提供了"服务注册"和"流量分发"功能。
服务注册能够理解为是"依赖倒置"pattern的一种应用,上游服务提供注册接口,由下游服务发起注册。这样可使新开发服务对已有服务的影响最小。你可能会问"不是要在上游服务作聚合吗"? 实际状况是不少服务实际上是相对比较独立的,聚合的状况其实不会太多,也就是说一次请求不少状况只有一个AtomicService完成响应就够了。
流量分发也有不一样的作法,一种作法是全量转发,由下游服务本身决定是否/如何处理。这样对下游服务来讲是最好的,可是缺点是须要服务节点能承受全流量的压力。 另外一种作法是在入口处先对query作必定分析,而后将服务id放入请求中,做为后续转发的一个依据。这种作法的缺陷是全部服务须要在一个地方完成query解析,也有单点带来的维护等问题。综合比较咱们采用的是后者。
注册语意相似:设计
[@service]
name : any-service
address : 100.100.100.100:1234
accept_ids : [1, 2, 10 - 20]日志
服务调用要解决的是服务加载和服务发现的问题。若是每一个服务都是单独的进程,能够直接加载到rpc server中,向上注册的时候同时声明本身的server地址及端口就能够了,现成的rpc实现有grpc。但总有一些缘由(好比性能要求、或者由于部署机制跟不上),要求service能在一个进程里面跑,同一个进程的service能够经过函数call而不是rpc调用,那将service放入一个容器(container)中,是一个不错的选择。
在咱们的实现中,service是加载进一个container的,请求经过container进行转发。当向上游service注册时,须要声明本身在哪一个container(这样作的一个问题是当service切换container时,须要修改上游。更好的作法能够考虑注册时只声明服务名,service和container的关系经过ZK之类的东西进行管理。咱们没这样作是由于目前为止service和container的关系是稳定的)。这样container就能够发现待调用service是在container内仍是外,若是是内部则经过函数call调用,外部则用rpc。
并非任何service均可以加载到相同的container中,这对service的隔离是一个很大的限制,实际使用中咱们会综合考虑服务的稳定性、开发、维护等因素。code
在线服务每每会对流量进行抽样而后进行小流量实验,每个实验其实就是一个不一样的service,流量分发时除了service id,可能还会考虑到抽样id,原理大体差很少。 一些通用组件的开发这里先不作描述了。