有状态服务或者说数据服务,上线遇到问题很棘手,回滚无济于事;而且数据加载通常都很慢,部署时间长;最终导致不敢修改代码,谨小慎微;服务质量也是能忍就忍,不愿意深度优化。在我负责顺风车LBS以来,感受愈加强烈;区别于无状态服务,数据服务的几个方面需要格外关注。(此处假设数据服务类似redis基于内存,数据量大到需要磁盘存储,关注点会有所不同。)
下面挑一些关键点简述。
多个分区,通过hash桶、一致性哈希等方式做数据分片,将数据映射到不同的分区;每个分区多个主节点,数据全量写入,上层服务主动做负载均衡读;单机服务故障不会影响在线业务。
由于分区存在多个角色相同的服务,都接受分区全量数据,数据一致性格外重要;多主架构,上游或者proxy保证数据一致性,如通过RPC请求:
但是RPC会有很大的坑,写请求超时时间难定义,数据部分写入不成功后消息重试容易造成雪崩。这时很容易想到通过mq解耦:
目前顺风车LBS类似这种结构,实际上维护代价也比较高。每台机器都要全量数据,不得不单独定义消费组名(集群消费不保证正常消费);机器数量增多,会有海量consumer group。
如果对数据实时性要求不高,mq同步是非常不错的选择。
主节点处理写操作,同步数据给从节点,一般从节点处理读请求;数据一致性和实时性可以通过主节点保证。主节点挂掉,需要用raft等重新选主;或者通过配置文件指定主从,人工介入选主。
所谓多主从,是指多个分区,分区内是主从结构;key映射分区如前所述。
以redis为例,数据同步通过数据文件和命令操作实现。初次同步master将数据文件完整发送给slave,后者load至内存;随后增量同步,逐命令或者定时同步写操作。
通常master和slave会维护类似binlog offset的偏移量,断线同步时提高速度。如RocketMQ主从同步,主从服务器建立长连接,更新携带offset信息的commitlog数据,维护数据一致。
RocketMQ通过配置文件指定主从,不会有选主这个过程,因此压根不涉及zk、raft等;redis则使用raft选主。
为了更合理的设计锁,通常都会自研一些数据结构,存储数据,提供快速读写功能。redis由于单线程设计,并没有过多考量,但还是设计了不少优秀的数据结构,如hash、跳表等。
数据往往可以以层次划分,连文件系统和操作系统都做层次化设计。对应数据服务,把锁分散在各层,尽量减少锁等待。
以一个多级hash+跳表结构为例,操作跳表时,锁粒度已经可以非常细。
内存数据和binlog哪个先写?binlog文件多久刷盘?写文件和刷盘是否在一个线程/进程?通常来讲需要先写binlog,确保服务重启时数据正常,然后写内存并返回。
最简单的持久化用leveldb,使用方便,接口清晰,稳定性毋庸置疑;而且leveldb写入速度极快,适合持久化。
自研binlog文件,可以实现更强大的功能:持久化文件配合内存数据结构,预分配+内存映射,快速加载;多种刷盘方式,配合无锁队列,加快写入速度;学习leveldb的merge方法,合并操作文件。
服务间通信通常使用thrift/pb(json/http还是略重,不太适合后端服务;且thrift对网络的封装足够好),但是直接拿来用并不好,会对应用产生依赖,后续修改后患无穷。
类似下面要说的功能边界划分,对于数据格式,也要摆脱对上层的依赖;同时需要考虑扩展性,增删字段或者类型变化,上下兼容。加个header是不错的选择:
struct Header {
int magic;
int version;
int nsize;
};
struct Data {
Header header;
int dsize;
void* data;
};
还有两个无状态服务也会面临的重点,功能边界划分
和线下环境搭建
:内部数据服务不同于开源项目,常常会与业务逻辑耦合,提高性能,丰富功能;但是边界模糊最终会导致代码逻辑混乱,层次复杂;清晰的边界划分至关重要。QA喜欢直连线上环境——数据充分,便于发现问题;通过搭建良好的线下环境,避免线上数据被污染,配合数据校验等工具,确保新功能、降低风险。
经常有人问我,为什么不直接用redis?redis作为强大的通用缓存/存储系统,并不能满足特定需求;例如网约车行业,数据检索至少也需要经纬度、时间等。自研数据服务听起来非常高大上,高性能数据存储、分布式架构设计、解决业务痛点,对外宣传的一把好手;实际上只要根据业务场景,合理分析,完成稳定高效的数据服务非常简单。