Feb14

【转载】斗鱼直播平台后端RPC架构浅析

Author: 陈厚道  Click: 3969   Date: 2017.02.14 @ 14:20:13 pm Category: 架构

# 背景

# 关键设计点

## 模块化

## 资源隔离

## 权限控制

### RPC框架的需求分析和概要设计

#### 发展与现状

- RPC框架指的是能够完成RPC调用的解决方案,除了点对点的RPC协议的具体实现之外,还可以包含服务的发现与注销,提供服务的多台Server的负载均衡、服务的高可用等更多的功能,目前的RPC框架大致有两种不同的侧重方向,一种偏重于服务治理,另一种偏重于跨语言调用。

- 开源的RPC框架介绍:Dubbo、DubboX、Thrift、Motan。其中Dubbo,Dubbox,motan是java生态下的偏向服务治理的RPC框架,Thrift是偏重于跨语言的调用的RPC。

#### RPC框架提供的主要功能

- 服务发现:服务发布、订阅、通知

- 负载均衡:支持一致性Hash、随机请求、轮询、最少连接数优先、低并发优先等分发原则

- 高可用策略:失败重试(FailOver)、快速失败(FailFast)、异常隔离(Server连续失败超过指定次数置为不可用,然后定期进行心跳探测)

- 其他: 调用统计、权限控制、安全、调用链追踪、日志

#### DY RPC框架交互流程1

DY RPC框架中有服务提供方 RPC Server,服务调用方RPC Client,服务注册中心MessageServer三个角色。该框架的RPCServer主要现在用c++写的服务,RPC Client包括php或者RPCServer。

1. RPC Server向MessageServer集群的某个节点B注册服务,并保持长连接。该MessageServe r B节点会通知集群的所有节点。

同时MessageServer B节点也会定时把注册到该节点的RPCServer的服务配置信息同步到 MessageServer集群。

2. RPCClient会连接到MessageServer集群的某个节点A,发起RPC调用。MessageServer A节点会根据RPC调用的参数(服务提供方的ID,GroupID、负载均衡策略等)选择一条合适的

RPC调用链路,比如RPCClient->MessageServerA->MessageServerB->RPCServer,最终到达某个RPCServer,进行函数调用。其中一个RPC调用最多会经过2个MessageServer节点,最少会经过1个MessageServer节点。

3. 当某个RPC Server发生变更时,通过广播的方式,MessageServer集群的所有节点也能比较实时的感知到某个RPCServer发生变更。

TODO RPC流程交互图

#### DY RPC功能模块划分

1. MessageServer在RPC框架这个功能上应该提供的功能,包括服务的注册和发现模块、协议序列化模块、心跳检测模块、负载均衡算法模块,RPC路由模块、失败重试策略模块、超时丢弃策略模块、消息持久化模块。

2. RPCServer要包含RPC治理的组件,主要功能包括RPC的统计、RPC的频率控制、RPC的安全性控制。

##### RPCServer可用性检测模块

每个服务默认都要实现一个类似Ping Pong的 Request和Response,用来给直连RPCServer的MessageServer探测RPCServer是否可用提供依据。不能简单的依赖心跳消息来探测RPCServer是否可用。

##### 负载均衡模块

MessageServer把RPC请求转发给RPCServer Group时,需要支持的负载均衡算法:

1. 随机法(已实现)

2. 轮询法(已实现)目前在生产环境用的这种算法,负载较不均衡。

3. 组内Hash法(已实现)

4. TODO 最少连接法 (最靠谱的负载均衡做法)

斗鱼采用的这些负载均衡算法可以参考这篇微信文章的介绍:http://mp.weixin.qq.com/s/PAOvmzraVlAMECL-PZs2Pg

看服务器响应自己请求的速度就可以判断应该把下一个请求发到哪个服务器端。

具体说是选择活动请求(已经发出去的请求收到响应)数目最少的那个服务端。 只要根据自己以往的调用情况就能做出判断。

5. TODO: 目前的消息系统只支持点到点、点到组。目前还暂不支持点到组内的某个节点的负载均衡算法。

##### 失败重试策略模块

在RPCClient直连的MessageServer上实现RPC失败重试的策略。

- 只有幂等的RPC调用才能重试。

##### 超时丢弃策略模块

在RPCServer的业务层实现超时丢弃的策略,应用场景:发送火箭超时时,客户端提示发送失败,其实是在鱼翅交易服务器出现性能抖动导致。最后的结果就是鱼翅服务器扣除了鱼翅,但是客户端提示发送火箭失败,比较严重的情况是,用户以为提示失败时不会消耗鱼翅,所以不断重新发送火箭。

针对这种类型的RPC,RPCServer的业务层可以根据RPC的配置规则+RPC发起时间来决定是否直接丢弃该RPC。

##### 消息持久化模块

- 在调用RPC时如果指定可达时,才触发消息持久化的机制。

- 因为RPC的调用链最多需要经过4个节点(RPCClient->MessageServerA->MessageServerB->RPCServer),导致RPC不可达到的情况较为复杂,如果采用自研的方案做消息持久化的话,我们可以假设MessageServer的集群比较稳定,RPCServer较不稳定,所以我们持久化的方案是在和RPCServer直连的MessageServer上实现。

- MessageServer上做持久化具体设计要点:

- 正常流程:

- MessageServer将RPC请求转化为消息,以RPCServer的模块id为Key,将消息存入Redis的队列,我们将这个消息称为MessageData;

- 将RPC请求的MessageID作为Key,Value作为保留字段设计,存入Redis的String,我们将这个数据称为MetaData,同时设置这个Key的过期时间为10分钟(暂定),这个操作和上面的操作作为Redis的一个事务来执行;

- 执行完上面的事务后,直接调用RPC的Response,返回给RPCClient;

- RPCServer集群的某个节点从Redis队列取出MessageData,执行RPCHandler。

- 异常流程1:

- 如果在执行RPCHandler的过程中,RPCServer异常,就只会影响一条MessageData。可以通过一些辅助脚本来做补单,考虑一种策略来实现自动化的补单。

- 异常流程2:

- MessageServerA->MessageServerB网络抖动 或者 MessageServerB->Redis的网络抖动都会导致MessageData不能进入队列;

- 在和RPCClient直连的MessageServerA一段时间(先暂定10s)没有收到RPCResponse,就会触发重试机制,重试的上限次数暂定20次,确保整体重试的时间小于MetaData过期的时间就可以,重试流程进入到MessageServerB节点时,如果是重试RPC,查找Redis队列是否有这个MessageData,如果不存在,则执行正常流程。如果存在,则丢弃本次重试,说明上一次重试已经成功了。

##### 增加RPC追踪链日志

- 在RPCClient直连的个MessageServer上给RPC请求赋予一个Global的RPCID;

- RPCID可以从IDMakerServer集群获得,通过一次获得一批ID来获得良好的性能;

- 在RPC经过的每个节点,都需要有规划统一的格式,并上报给大数据平台;

- 在大数据后台,可以根据RPCID查找整个RPC调用链上的信息。

##### RPC治理组件

- RPC调用统计:每个RPC入口增加统计信息,当rpc进入内部业务函数后也有一次统计,统计信息汇入大数据实时统一日志

- RPC频率控制:某个时间单位内,RPC调用不得超过某个数量;如果有超过,则报警。在频率控制粒度方面,采取如下控制策略和监控策略。

- 每个服务的所有RPC在单位时间内的调用频率控制,超出则报警;

- 某个RPC在单位时间内的调用频率控制,超出则报警;

- 定时统计每个Client来源在每个RPC的调用次数,并按照统一格式上传给大数据平台,大数据平台提供按照Client来源、时间查找RPC调用次数的Top 10的类似功能;

- 大数据平台定时对比RPC的历史调用次数和当前调用次数,超过一定的比例就报警。

- RPC安全策略:

- 可以随时关闭某个RPC、某个服务的所有RPC的安全策略;

- ip验证:给一个ip白名单,这个白名单才能发起RPC调用,不建议按照每个RPC调用单独设置ip白名单

- 口令验证:针对某个RPC、某个服务单独设置密码,对大都数服务都设置成统一的密码,不建议针对每个RPC或者每个服务都单独设置密码。因为除了密码,还有一个摘要认证加密算法才能破解RPC的协议。现在密码是运维维护的,摘要认证加密算法是开发维护的。所以不建议对密码的粒度控制得过细

- RPC手动降级:可以随时关闭某个RPC;也可以根据Client来源关闭某个RPC,但对其他Client来源是开启的。

- RPC自动降级: TODO

- 配置文件格式:参考详细设计文档 by 李明

#### 关键数据结构

1. 服务注册协议

```

struct MC_MsgLoginReqNew : public MessageRoot

{

uint8_t  _version;

DWORD    _uid;

char      _user_name[33];

char      _password[33];      //之前的口令字段依旧不使用

int      _module_id          //模块id

};

```

2.RPCClient Request基本结构,同样包括GroupRPCReq(组内随机调用),GroupRPCReq2(组内hash调用)的

```

struct MC_RPC_Req_New : public MessageRoot< MCT_RPC_Req_New, MC_RPC_Req_New >

{

uint8_t _version;        // 版本号

int64 _rpc_global_id;    // 每次调用需要从idMakderServer获得唯一id,RPC追踪链需要依赖该id来识别

int _rpc_option;          // 包括RPC可达,重试,超时丢失等标志,不可叠加

int32 _user_data;          // 自定义用户数据

int _rpc_retry_count;      // 0表示第一次请求,每重试一次+1

int _invoker_id;          // 调用者的ID

int _invoker_moudule_id;  // 调用者的模块id

uint32_t _invoker_ip;      // 调用者的ip

int _call_token;        //调用标识,由调用者设置,返回结果时必须携带此token,否则调用者无法区分是哪一次调用

int _recvier_id;      // 接收者的serverID

uint32_t _req_time    // 请求时间戳

int64_t _random_num;  // 随机数 没有口令配置此项可以不填

uchar _password[32];  // 口令,由 随机数+ 模块id+ 函数名+ 配置文件的口令+ 时间戳 的字符串 一次md5获得,服务器使用同字段md5对比校验,没有口令配置此项留空即可

char _func_name[128]; //函数名

char _text_data[1];  //SttEncoding存储函数体,包括函数名、参数名/参数值

};

```

> _rpc_global_id,_invoker_moudule_id,_invoker_ip这个由调用方直连的第一个MessageServer直接赋值

> _version,_req_time,_random_num,_password,由RPCClient生成,RPCServer校验

> _rpc_retry_count,表示重试的次数,这个由调用方直连的第一个MessageServer发起重试策略时+1

> _rpc_option,包括RPC可达,重试,超时丢弃等标志,现在不可叠加,以后可支持叠加,常见的场景是:

1. RPCClient不太关注返回结果的、关键数据更新类的RPC,建议指定RPC可达。

2. RPCClient非常关注返回结果提示,但又不支持重试的(非幂等RPC),建议指定超时丢弃标志

3. RPCClient非常关注返回结果提示,该RPC又支持幂等,建议指定重试标志

4. _user_data:根据不同的RPC标志,可以指定特定的含义.比如指定最大重试次数或者超时丢弃的时间

> GroupRPCReq(组内随机调用),GroupRPCReq2(组内hash调用)的数据结构也需要同时更新。

3. RPCServer Response基本结构

```

struct RPC_RespNew

{

uint8_t _version;      // 版本号

int64 _rpc_global_id;  // RPC全局唯一id

int recvier_id;        // 接收者的ID;如果是按组接收,此值由MessageServer修改为具体的接收者ID

int invoker_id;        // 调用者的ID

int call_token;        //调用标识,由调用者设置,返回结果时必须携带此token,否则调用者无法区分是哪一次调用

char text_data[1];      //SttEncoding存储调用结果

}

```

#### 以送火箭场景为场景描述架构重构的思路

1. php调用发送火箭RPC接口给鱼翅交易服务器,鱼翅服务器完成RPC调用,并且是把这个消息发送给所有的ChatRoom。

2. 鱼翅交易服务器把发送火箭这个RPC封装成NetMessage通过ChatRoom发送给RoomMaster,RoomMaster找到人气值前50的房间,并向人气值前50的房间的ChatRoom发送火箭广播的NetMessag,ChatRoom再把广播消息发送给MessageRepeater

3. ChatRoom通过NetMessage把发送火箭这个消息事件发送给排行榜服务器

4. ChatRoom通过NetMessage把发送火箭这个消息事件发送给经验服务器

5. ChatRoom发送创建红包RPC给红包服务器

目前的业务流程的主要弊端如下:

1. ChatRoom和大都数服务耦合非常紧密,据我了解,c++的各个小组经常存在同时维护ChatRoom的情况。

2. 同样,新增一个和送火箭相关的服务,ChatRoom也需要增加相关接口。

3. ChatRoom通过RPC、NetMessage和其他业务交互时,如果网络出现抖动或者其他业务在维护或者不稳定时,都会导致数据的丢失,比较影响用户的体验。

针对送火箭这个业务流程,我个人认为比较优雅的架构(新架构)如下:

1. php调用发送火箭RPC接口给鱼翅交易服务器,鱼翅服务器完成RPC调用

2. 鱼翅交易服务器把发送火箭这个RPC封装成消息事件,发送给消息队列服务器。

3. 红包服务器、经验服务器、排行榜服务器、RoomMaster都通过订阅的方式订阅到了发送火箭这个消息。这些服务器按照自己的业务规则处理该消息事件!

4. RoomMaster找到人气值前50的房间,并向人气值前50的房间的ChatRoom发送火箭广播的NetMessag,ChatRoom再把广播消息发送给MessageRepeater。

新架构的优点如下:

1. ChatRoom和其他服务已经完全解耦。

2. 如果新增一个和送火箭相关的服务,鱼翅服务器的逻辑也不用调整。新增的服务只需要订阅送火箭的消息队列就可以了。

3. 消息队列服务器是一个稳定的第三方服务,基本是不用维护的。其他比如红包服务器、经验服务器、排行榜服务器的不稳定,都不会导致数据的丢失。

老框架迁移到新框架下的推进计划:

1. 先挑选送火箭这个业务进行重构,其他业务流程仍然兼容老的RPC的通信方式;

2. 逐步梳理c++组的业务流程,挑选业务流程逐一进行重构;

3. 第一个业务流程的重构预估时间大概在3周左右,后面的每个业务流程重构预估在1周左右,在3-4个月的时间内梳理完所有流程。

## 当前底层框架可以优化的点

1. MessageServer集群可以优雅的增加、删除、修改(同时删除、增加来实现)节点,现在修改某个节点的ip需要重启整个集群?

2. 把弹幕的MessageServer集群和RPC的MessageServer集群分离

3. 协议序列化框架改成ProtoBuffer,可以逐个协议升级

4. MessageServer的通信链路自动检测,防止出现某个节点异常之后很久才发现

## 统一日志

**TODO:本周5和c++组讨论之后再确定**

## 近期之内主要的工作项

WorkItem | 优先级 | 备注

---|---|---



TAG:   douyu 斗鱼 rpc

    评论
    • 提交

    分类

    标签

    归档

    最新评论

    shallwe99在10:21:17评论了
    【原创】如何在微信小程序开发中正确的使用vant ui组件
    默一在09:04:53评论了
    Berkeley DB 由浅入深【转自架构师杨建】
    Memory在14:09:22评论了
    【原创】最佳PHP框架选择(phalcon,yaf,laravel,thinkphp,yii)
    leo在17:57:04评论了
    shell中使用while循环ssh的注意事项
    呵呵哒啦在15:06:33评论了
    【原创】最佳PHP框架选择(phalcon,yaf,laravel,thinkphp,yii)

    我看过的书

    链接

    其他

    访问本站种子 本站平均热度:6898 c° 本站链接数:29 个 本站标签数:459 个 本站被评论次数:93 次