CloudData是什么?
任何一个App都需要一个Server,我们认为,移动开发者(或组织)不应该把精力放在这些事情上面:
1.搭建后端Server服务;
2.编写后端Server代码;
3.设计Server底层数据存储架构;
4.关注Server的高可用、可扩展、负载均衡、高性能等诸多繁琐问题。
这些事情可能会耗掉你80%以上的时间和精力,结果服务可用性、稳定性、可扩展性等都可能不尽人意。
我们还认为,你应该把更多的精力聚焦在业务和App开发上面,这样不仅节约了时间和金钱,更重要的是你能把所有精力都放在业务本身上面,让你的应用在竞争中脱颖而出,领先竞争对手。
那么,说这么多,CloudData到底是什么鬼?
简单来讲就是针对App开发者提供的统一的后端对象数据存储服务,让用户不再关心Server端数据存储的问题,CloudData具有关系型、半结构化、json格式及单个对象具有事务性等特点,采用MongoDB 3作为存储系统,至于CloudData更多细节请参考MaxLeap CloudData相关文档。
CloudData架构非常的简单,从上往下看,包括API Server、数据访问逻辑处理层和底层数据存储层,下面,我们分别介绍三个模块的具体实现原理。
API Server
在API Server层面,我们的语言采用的是java,针对java,当前比较流行的方式可能是:选择一个web 框架,可能是spring mvc,使用java servlet,然后选择一款 java web 容器,可能是tomcat或者jetty等,但我们并没有采用这样方式,因为,在我们看来,这种方式有几个缺点:架构显得有些笨拙,每次启动都需要部署到java web 容器中,不够轻量、同步通信,并发上不去,性能较低。因此,通过综合考量,我们选择了vert.x web框架,vert.x 基于事件驱动、非阻塞通信机制,这意味着,只需要很少的线程就能应付大量的并发,同时它也更加的轻量,拥有更高的性能,启动vert.x web服务只需要几十毫秒到几百毫秒之间,使用也是异常的简单,只要你稍稍懂一点http协议及网络编程几乎可以说是零学习成本:
nginx
server {
listen 80;
server_name
static
-test-47242.onmodulus.net;
root /mnt/app;
index index.html index.htm;
location /
static
/ {
try_files $uri $uri/ =404;
}
location /api/ {
proxy_pass http:
//node-test-45750.onmodulus.net;
}
}
HttpServer server = vertx.createHttpServer();
Router router = Router.router(vertx);
router.route("/index").handler(routingContext ->
{ HttpServerResponse response = routingContext.response();
response.putHeader("content-type", "text/plain");
// Write to the response and end it
response.end("Hello World Maxleap!"); });
server.requestHandler(router::accept).listen(10086); |
需要注意的是,vert.x web基于EventLoop单线程模式,所以,在写web服务的时候,不能写同步处理逻辑,否则会阻塞主线程,导致其他请求不能得到响应;另外,有异步编程经验的同学一定感受到这样的痛苦:callback方法泛滥成灾,大量的flatMap方法调用导致也导致代码的可读性差,除此之外,我们实在是找不到vert.x web框架的任何缺点。当然,这些缺点也并不是不可以避免,基于此,我们对vert.x web,稍稍加了一点东西,改变了一些实现策略:我们在逻辑处理阶段将异步变成同步,在框架后面,我们在变成异步,并且增加了对JAX-RS 部分规范支持,如果之前你觉得,基于vert.x web写rest接口还有一点点的学习成本,但现在基于我们改变之后的vert.x web写rest接口,你会发现跟以前并没有两样:
@GET @Path(":className/:objectId")
public void get(RoutingContext context) {
func(context, (ctx, cloudCode, appId, className, principal) -> {
LASObject doc = lasDataEntityManager.get
(appId, className, principal, new ObjectId(context.request().getParam("objectId")));
if (doc == null) {
ctx.response().end("{}");
} else {
ctx.response().end(MongoJsons.serialize(doc));
} }); } |
更多的API Server相关细节请参考Maxleap的开源BaaS系统实现 https://github.com/MaxLeap/MyBaaS
数据访问层
这一层包含了CloudData的核心逻辑实现,相对来讲,是CloudData设计中最复杂的一层,当然,仅仅只是相对,所有的逻辑处理都在这一层,而Pandora是其具体实现,所以,我们重点会讲一讲Pandora的设计思路
Pandora是古希腊神话中最美的女人,她充满诱惑,携带危险来到人间,当然,在我们看来,危险与诱惑跟美丽的女人无关,如果说美丽的人会让你身处危险之中,那就请让我万劫不复吧。
扯远了,Pandora是Maxleap统一数据访问层的实现,主要是针对mongo数据的操作,所以,不仅仅是用在CloudData中,在Maxleap中所有应用中,需要访问mongo数据库的都是通过Pandora来进行的,并且提供了异步和同步两种接口
Pandora主要实现了两种模式的数据访问,CloudData和NativeData。
Clouddata是Maxleap App数据的访问,凡是访问应用的数据,都是通过这个模块,CloudData数据的访问稍显复杂一些,包含Pointer、Relation等自定义数据结构的实现;数据ACL的实现;自定义操作指令如$relationTo等
NativeData是对mongo指令友好的封装,与mongo原生指令并没有两样。
Pandora最为核心的功能是实现了资源限制和数据库访问的路由策略,这对数据库进行平滑的动态扩展及迁移提供了可能性,当然,目前Maxleap也实现了这一点,当用户的应用因为数据量、访问量上升需要升级后台mongo数据库的时候,这一切都是无感知的,不会影响你的线上服务照常运行,当然,某些特殊情况下,可能需要短暂的停顿服务几分钟,并且仅仅是写停顿,读不受影响。
在Maxleap项目中,使用Pandora最大的好处是,开发人员无需关心后台mongo的架构,数据一致性、mongo服务升级降级等诸多繁琐问题,当然,你甚至连数据存储在那个mongo集群都不要知道,因为,使用Pandora我实在想不到你还需要了解数据存储在哪里的理由。
Clouddata数据存储架构设计
如果你对Baas有些许,如果你去Maxleap提供的服务有所了解,我想,你一定会关心Cloudapp后端数据存储架构是怎么设计的,成千过万的应用数据我们是怎么存储的、你的应用可用性问题、服务是否有足够的保障,接下来,我们正要介绍这一点。
先看一下业界某BaaS服务的Clouddata的设计:
(图片来自网络)
在我们看来,这是一种糟糕的设计,如果我们没有理解错的话,所有的用户数据都在一个大集群里面,后端是一个大的sharded集群,所有的用户数据的访问都会经由一组mongos路由服务(毫无疑问,这绝对是一个风险,试想,如果mongos出了问题,整改后端数据存储就会down掉),当然,这并不是主要问题,主要问题在于:
数据在一个庞大的集群中,出现问题的概率也会大很多,比如有10000个应用,那么出现问题的概率也会大10000倍,1个应用出现一次问题,那么整个服务都会因为某一个应用出现问题而受到影响。
数据规模大了,运维困难,又如,如果mongo异常down掉,恢复时间绝对的是一个煎熬的过程,而这个时候,整个服务都不可用。
稳定性较差,每一个应用都有可能发送令人恶心的指令,可能会导致资源抢占(比如最常见的情况是没有索引的查询,会引起热数据污染,结果集返回大量数据,抢占了网络带宽等),这个时候,整个集群都会受到影响,可能导致整个集群的吞吐量下降,反应到应用层面,可能就是大量应用请求短时间超时,读写失败
这是我们Maxleap其中的一台产品Mongo Server所表现的性能,当然,你可能会惊讶于数据在磁盘也能够拥有4000/s的吞吐量,当然,这一切得益于我们使用SSD硬盘所带来的效果。这张图也告诉我们,尽可能的小心你的查询,当服务器趋于吞吐平稳的时候,不要造成内存数据污染,但如果是所有的应用在一个大集群,这种情况将不可避免,因为,你永远都不会知道用户会写怎样的查询指令,而一旦有这样的指令,影响将会扩散到整个大集群其他的应用。
\4. 效率低下,需要更多的硬件成本,比如,当你的读写压力变大的时候,你不得不加新的机器去copy集群的全量数据,造成磁盘和内存的浪费,因为你应用的热数据在每一个mongo节点都有(当然,为什么我列在第四点,可能你觉得这并不重要,硬件从来都不是问题,更重要的是,你觉得,公司从来都不缺这点钱)
好了,是时候看看我们的CloudData的设计了,当然,也是极其的简单,遵循3个原则:
用户资源隔离(毫无疑问,这点是最重要的);
动态可无限扩展(不管你有多少个库,1万还是100万);
分而治之。
基于以上3点,我们的设计是为每一个应用创建一个独立mongo集群,是不是太简单了,有木有,但是,我们认为这是最科学的,将大集群按照应用分解成小集群,大问题分解成小问题,以大化小,分而治之,不管在哪里,都是靠谱的,并且还特别有效,最近几年,火的不要不要的被广泛传播的软件架构方式‘微服务’不也是同样的道理么?当然,你可以说,微服务更倾向于‘单一职责’原理,当然,只要你高兴,随你怎么想。
也许,你可能会质疑这是否会浪费资源,因为,每个人都会有这样直观上的错觉,但是,事实上,不仅没有浪费资源,还会大大的节约资源,这一点,在上面已经讨论到,数据在一个大集群中,副本集越多,浪费的内存越多,磁盘越多。第二点,起一个mongo集群,并不会浪费多少内存。
当然更重要的是,我们并不是真正为每一个应用创建一个mongo集群,毕竟,这样做,很多时候并没有意义,因为,90%的应用可能并没有较高的访问量,50%的以上的应用,可能从来都不会被频繁访问。
所以,我们的策略是,当用户第一次注册Maxleap创建一个应用的时候,用户的数据放在一个默认的集群中,在Maxleap中,这个集群叫User Default Cluster,当你的用户有一定的访问量的时候(没有访问量也没有关系,只要你是我们的付费用户)我们会把你公司所有的应用部署在一个单独的mongo集群中,再往后,你应用访问量继续上升,我们就把你的应用在单独升级到一个mongo集群,当然,这一切的升级过程,都是平滑无缝的进行的,没有人能够感知的到,包括我们自己。
So,我们Maxleap CloudData是这样子的:
好了,关于Pandora及CloudData我们先简单介绍到这里,更多的细节我们到时候会专门拧一个细节出来讨论。
数据存储层
上面我们早已经知道,我们使用的Mongo作为Clouddata作为主要存储系统,更早之前,在我们mongo还在2.6之前,我们还使用了redis作为热数据缓存,但后来我mongo升级到3.0之后,变去掉了redis,所以,整个底层数据存储,就只剩下mongo,整个系统设计又进一步变得更加简单,之所以去掉redis,这不是我们所要讨论的重点,我们会在相关的文章中进行详细说明。
这里,我们重点介绍mongo的集群架构设计方案
集群特点:
每一个mongo集群都是一个复制集:1主1从1投票节点,还有一个备份节点(Norns-backup是我们自主实现的一个增量实时备份系统)
每一个mongo集群都在一个VPC网络里面
每一个mongo集群3个节点都在不同的可用区里面(不同的机房,机房是光纤直连,网络延迟大概咋500us)
部分mongo集群的数据存储在LVM上面
每一个mongo集群都使用SSD硬盘
每一个mongo集群我们都提供瞬时10倍以上的请求峰值
|