分布式系统常见问题总结(二)
分布式系统常见问题总结(二)
参考:
微信公众号:架构师之路
互联网分层架构的本质
上图是一个典型的互联网分层架构:
-
客户端层:典型调用方是browser或者APP
-
站点应用层:实现核心业务逻辑,从下游获取数据,对上游返回html或者json
-
数据-缓存层:加速访问存储
-
数据-数据库层:固化数据存储
如果实施了服务化,这个分层架构图可能是这样:
中间多了一个服务层。
同一个层次的内部,例如端上的APP,以及web-server,也都有进行MVC分层:
-
view层:展现
-
control层:逻辑
-
model层:数据
可以看到,每个工程师骨子里,都潜移默化的实施着分层架构。
那么,互联网分层架构的本质究竟是什么呢?
如果我们仔细思考会发现,不管是跨进程的分层架构,还是进程内的MVC分层,都是一个“数据移动”,然后“被处理”和“被呈现”的过程,归根结底一句话:互联网分层架构,是一个数据移动,处理,呈现的过程,其中数据移动是整个过程的核心。
如上图所示:
数据处理和呈现要CPU计算,CPU是固定不动的:
-
db/service/web-server都部署在固定的集群上
-
端上,不管是browser还是APP,也有固定的CPU处理
数据是移动的:
-
跨进程移动:数据从数据库和缓存里,转移到service层,到web-server层,到client层
-
同进程移动:数据从model层,转移到control层,转移到view层
数据要移动,所以有两个东西很重要:
-
数据传输的格式
-
数据在各层次的形态
先看数据传输的格式,即协议很重要:
-
service与db/cache之间,二进制协议/文本协议是数据传输的载体
-
web-server与service之间,RPC的二进制协议是数据传输的载体
-
client和web-server之间,http协议是数据传输的载体
再看数据在各层次的形态,以用户数据为例:
-
db层,数据是以“行”为单位存在的row(uid, name, age)
-
cache层,数据是以kv的形式存在的kv(uid -> User)
-
service层,会把row或者kv转化为对程序友好的User对象
-
web-server层,会把对程序友好的User对象转化为对http友好的json对象
-
client层:最终端上拿到的是json对象
结论:互联网分层架构的本质,是数据的移动。
为什么要说这个,这将会引出“分层架构演进”的核心原则与方法:
-
让上游更高效的获取与处理数据,复用
-
让下游能屏蔽数据的获取细节,封装
弄清楚这个原则与方法,再加上一些经验积累,就能回答网友经常在评论中提出的这些问题了:
-
是否需要引入DAO层,什么时机引入
-
是否需要服务化,什么时机服务化
-
是否需要抽取通用中台业务,什么时机抽取
-
是否需要前后端分离,什么时机分离
(网友们的这些提问,其实很难回答。在不了解业务发展阶段,业务规模,数据量并发量的情况下,妄下YES或NO的结论,本身就是不负责任的。)
更具体的分层架构演进细节,下一篇和大家细究。
总结
-
互联网分层架构的本质,是数据的移动
-
互联网分层架构中,数据的传输格式(协议)与数据在各层次的形态很重要
-
互联网分层架构演进的核心原则与方法:封装与复用
互联网分层架构之-DAO与服务化
互联网分层架构的本质,是数据的移动。
互联网分层架构演进的核心原则:
-
让上游更高效的获取与处理数据,复用
-
让下游能屏蔽数据的获取细节,封装
这些在上一篇《互联网分层架构的本质》中有详尽的描述,在实际系统架构演进过程中,如何利用这两个原则,对系统逐步进行分层抽象呢?咱们先从后端系统开始讲解。
本文主要解答两个问题:
-
后端架构,什么时候进行DAO层的抽象
-
后端架构,什么时候进行数据服务层的抽象
核心问题一:什么时候进行DAO层的抽象
一个业务系统最初的后端结构如上:
-
web-server层从db层获取数据并进行加工处理
-
db层存储数据
此时,web-server层如何获取底层的数据呢?
web-server层获取数据的一段伪代码如上,不用纠结代码的细节,也不用纠结不同编程语言与不同数据库驱动的差异,其获取数据的过程大致为:
-
创建一个与数据库的连接,初始化资源
-
根据业务拼装一个SQL语句
-
通过连接执行SQL语句,并获得结果集
-
通过游标遍历结果集,取出每行数据,亦可从每行数据中取出属性数据
-
关闭数据库连接,回收资源
如果业务不复杂,这段代码写1次2次还可以,但如果业务越来越复杂,每次都这么获取数据,就略显低效了,有大量冗余、重复、每次必写的代码。
如何让数据的获取更加高效快捷呢?
通过技术手段实现:
-
表与类的映射
-
属性与成员的映射
-
SQL与函数的映射
绝大部分公司正在用的ORM,DAO等技术,就是一种分层抽象,可以提高数据获取的效率,屏蔽连接,游标,结果集这些复杂性。
结论
当手写代码从DB中获取数据,成为通用痛点的时候,就应该抽象出DAO层,简化数据获取过程,提高数据获取效率,向上游屏蔽底层的复杂性。
核心问题二:什么时候要进行数据服务层的抽象
抽象出DAO层之后,系统架构并不会一成不变:
-
随着业务越来越复杂,业务系统会不断进行垂直拆分
-
随着数据量越来越大,数据库会进行水平切分
-
随着读并发的越来越大,会增加缓存降低数据库的压力
于是系统架构变成了这个样子:
业务系统垂直拆分,数据库水平切分,缓存这些都是常见的架构优化手段。
此时,web-server层如何获取底层的数据呢?
根据楼主的经验,以用户数据为例,流程一般是这样的:
-
先查缓存:先用uid尝试从缓存获取数据,如果cache hit,数据获取成功,返回User实体,流程结束
-
确定路由:如果cache miss,先查询路由配置,确定uid落在哪个数据库实例的哪个库上
-
查询DB:通过DAO从对应库获取uid对应的数据实体User
-
插入缓存:将kv(uid, User)放入缓存,以便下次缓存查询数据能够命中缓存
如果业务不复杂,这段代码写1次2次还可以,但如果业务越来越复杂,每次都这么获取数据,就略显低效了,有大量冗余、重复、每次必写的代码。
特别的,业务垂直拆分成非常多的子系统之后:
-
一旦底层有稍许变化,所有上游的系统都需要升级修改
-
子系统之间很可能出现代码拷贝
-
一旦拷贝代码,出现一个bug,多个子系统都需要升级修改
不相信业务会垂直拆分成多个子系统?举两个例子:
-
58同城有招聘、房产、二手、二手车、黄页等5大头部业务,都需要访问用户数据
-
58到家有月嫂、保姆、丽人、速运、平台等多个业务,也都需要访问用户数据
如果每个子系统都需要关注缓存,分库,读写分离的复杂性,调用层会疯掉的。
如何让数据的获取更加高效快捷呢?
服务化,数据服务层的抽象势在必行。
通过抽象数据服务层:
-
web-server层可以通过RPC接口,像调用本地函数一样调用远端的数据
-
数据服务层,只有这一处需要关注缓存,分库,读写分离这些复杂性
服务化这里就不展开,更详细的可参考《互联网架构为什么要做服务化?》。
结论
当业务越来越复杂,垂直拆分的系统越来越多,数据库实施了水平切分,数据层实施了缓存加速之后,底层数据获取复杂性成为通用痛点的时候,就应该抽象出数据服务层,简化数据获取过程,提高数据获取效率,向上游屏蔽底层的复杂性。
互联网分层架构是一个很有意思的问题,服务化的引入,并不是越早越好:
-
请求处理时间可能会增加
-
运维可能会更加复杂
-
定位问题可能会更加麻烦
千万别鲁莽的在“微服务”大流之下,草率的进行微服务改造,看似“高大上架构”的背后,隐藏着更多并未接触过的“大坑”。还是那句话,架构和业务的特点和阶段有关:一切脱离业务的架构设计,都是耍流氓。
这一篇先到这里,分层架构,还有很多内容要和大家聊:
-
后端架构,是否需要抽取中台业务,什么时机抽取
-
后端架构,是否需要前后端分离,什么时机分离
-
前端架构,如何进行分层实践
末了,再次强调下,互联网分层架构的本质,是数据的移动。
互联网分层架构演进的核心原则,是让上游更高效的获取与处理数据,让下游能屏蔽掉数据的复杂性获取细节。
互联网架构为什么要做服务化?
近期参加一些业界的技术大会,“微服务架构”的话题非常之火,也在一些场合聊过服务化架构实践,最近几期文章期望用通俗易懂的语言聊聊了个人对服务化以及微服务架构的理解,希望能给大伙一些启示。如果有遗漏,也欢迎大家补充。
一、互联网高可用架构,为什么要服务化?
【服务化之前高可用架构】
在服务化之前,互联网的高可用架构大致是这样一个架构:
(1)用户端是浏览器browser,APP客户端
(2)后端入口是高可用的nginx集群,用于做反向代理
(3)中间核心是高可用的web-server集群,研发工程师主要编码工作就是在这一层
(4)后端存储是高可用的db集群,数据存储在这一层
更典型的,web-server层是通过DAO/ORM等技术来访问数据库的。
可以看到,最初都是没有服务层的,此时架构会碰到一些什么痛点呢?
【架构痛点一:代码到处拷贝】
举一个最常见的业务的例子->用户数据的访问,绝大部分公司都有一个数据库存储用户数据,各个业务都有访问用户数据的需求:
在有用户服务之前,各个业务线都是自己通过DAO写SQL访问user库来存取用户数据,这无形中就导致了代码的拷贝。
【架构痛点二:复杂性扩散】
随着并发量的越来越高,用户数据的访问数据库成了瓶颈,需要加入缓存来降低数据库的读压力,于是架构中引入了缓存,由于没有统一的服务层,各个业务线都需要关注缓存的引入导致的复杂性:
对于用户数据的写请求,所有业务线都要升级代码:
(1)先淘汰cache
(2)再写数据
对于用户数据的读请求,所有业务线也都要升级代码:
(1)先读cache,命中则返回
(2)没命中则读数据库
(3)再把数据放入cache
这个复杂性是典型的“业务无关”的复杂性,业务方需要被迫升级。
随着数据量的越来越大,数据库需要进行水平拆分,于是架构中又引入了分库分表,由于没有统一的服务层,各个业务线都需要关注分库分表的引入导致的复杂性:
这个复杂性也是典型的“业务无关”的复杂性,业务方需要被迫升级。
包括bug的修改,发现一个bug,多个地方都需要修改。
【架构痛点三:库的复用与耦合】
服务化并不是唯一的解决上述两痛点的方法,抽象出统一的“库”是最先容易想到的解决:
(1)代码拷贝
(2)复杂性扩散
的方法。抽象出一个user.so,负责整个用户数据的存取,从而避免代码的拷贝。至于复杂性,也只有user.so这一个地方需要关注了。
解决了旧的问题,会引入新的问题,库的版本维护与业务线之间代码的耦合:
业务线A将user.so由版本1升级至版本2,如果不兼容业务线B的代码,会导致B业务出现问题;
业务线A如果通知了业务线B升级,则是的业务线B会无故做一些“自身业务无关”的升级,非常郁闷。当然,如果各个业务线都是拷贝了一份代码则不存在这个问题。
【架构痛点四:SQL质量得不到保障,业务相互影响】
业务线通过DAO访问数据库:
本质上SQL语句还是各个业务线拼装的,资深的工程师写出高质量的SQL没啥问题,经验没有这么丰富的工程师可能会写出一些低效的SQL,假如业务线A写了一个全表扫描的SQL,导致数据库的CPU100%,影响的不只是一个业务线,而是所有的业务线都会受影响。
【架构痛点五:疯狂的DB耦合】
业务线不至访问user数据,还会结合自己的业务访问自己的数据:
典型的,通过join数据表来实现各自业务线的一些业务逻辑。
这样的话,业务线A的table-user与table-A耦合在了一起,业务线B的table-user与table-B耦合在了一起,业务线C的table-user与table-C耦合在了一起,结果就是:table-user,table-A,table-B,table-C都耦合在了一起。
随着数据量的越来越大,业务线ABC的数据库是无法垂直拆分开的,必须使用一个大库(疯了,一个大库300多个业务表 =_=)。
【架构痛点六:…】
二、服务化解决什么问题?
为了解决上面的诸多问题,互联网高可用分层架构演进的过程中,引入了“服务层”。
以上文中的用户业务为例,引入了user-service,对业务线响应所用用户数据的存取。引入服务层有什么好处,解决什么问题呢?
【好处一:调用方爽】
有服务层之前:业务方访问用户数据,需要通过DAO拼装SQL访问
有服务层之后:业务方通过RPC访问用户数据,就像调用一个本地函数一样,非常之爽
User = UserService::GetUserById(uid);
传入一个uid,得到一个User实体,就像调用本地函数一样,不需要关心序列化,网络传输,后端执行,网络传输,范序列化等复杂性。
【好处二:复用性,防止代码拷贝】
这个不展开叙述,所有user数据的存取,都通过user-service来进行,代码只此一份,不存在拷贝。
升级一处升级,bug修改一处修改。
【好处三:专注性,屏蔽底层复杂度】
在没有服务层之前,所有业务线都需要关注缓存、分库分表这些细节。
在有了服务层之后,只有服务层需要专注关注底层的复杂性了,向上游屏蔽了细节。
【好处四:SQL质量得到保障】
原来是业务向上游直接拼接SQL访问数据库。
有了服务层之后,所有的SQL都是服务层提供的,业务线不能再为所欲为了。底层服务对于稳定性的要求更好的话,可以由更资深的工程师维护,而不是像原来SQL难以收口,难以控制。
【好处五:数据库解耦】
原来各个业务的数据库都混在一个大库里,相互join,难以拆分。
服务化之后,底层的数据库被隔离开了,可以很方便的拆分出来,进行扩容。
【好处六:提供有限接口,无限性能】
在服务化之前,各业务线上游想怎么操纵数据库都行,遇到了性能瓶颈,各业务线容易扯皮,相互推诿。
服务化之后,服务只提供有限的通用接口,理论上服务集群能够提供无限性能,性能出现瓶颈,服务层一处集中优化。
啊,业务层是否也需要服务化?
《互联网分层架构的本质》简述了两个观点:
-
互联网分层架构的本质,是数据的移动
-
互联网分层架构演进的核心原则:是让上游更高效的获取与处理数据,让下游能屏蔽数据的获取细节
《分层架构:什么时候抽象DAO层,什么时候抽象数据服务层》中的观点是:
-
当手写代码从DB中获取数据,成为通用痛点的时候,就应该抽象出DAO层,简化数据获取过程,提高数据获取效率,向上游屏蔽底层的复杂性
-
当业务越来越复杂,垂直拆分的系统越来越多,数据库实施了水平切分,数据层实施了缓存加速之后,底层数据获取复杂性成为通用痛点的时候,就应该抽象出数据服务层,简化数据获取过程,提高数据获取效率,向上游屏蔽底层的复杂性
文本将要解答的问题是:
-
基础数据的访问需要服务化,业务层是否需要服务化
-
如果需要服务化,什么时候服务化
基础数据的访问服务化之后,一个业务系统的后端架构如上:
-
web-server通过RPC接口,从基础数据service获取数据
-
基础数据service通过DAO,从db/cache获取数据
-
db/cache存储数据
随着时间的推移,系统架构并不会一成不变:
-
随着业务越来越复杂,业务会不断进行垂直拆分
-
随着数据越来越复杂,基础数据service也会越来越多
于是系统架构变成了上图这个样子,业务垂直拆分,有若干个基础数据服务:
-
垂直业务要通过多个RPC接口访问不同的基础数据service,service共有是服务化的特征
-
每个基础数据service访问自己的数据存储,数据私有也是服务化的特征
这个架构图中的依赖关系是不是看上去很别扭?
-
基础数据service与存储层之前连接关系很清晰
-
业务web-server层与基础数据service层之间的连接关系错综复杂,变成了蜘蛛网
再举一个更具体的例子,58同城列表页web-server如何获取底层的数据?
-
首先调用商业基础service,获取商业广告帖子数据,用于顶部置顶/精准的广告帖子展示
-
再调用搜索基础service,获取自然搜索帖子数据,用于中部自然搜索帖子展示
-
再调用推荐基础service,获取推荐帖子数据,用于底部推荐帖子展示
-
再调用用户基础service,获取用户数据,用于右侧用户信息展示
-
…
如果只有一个列表页这么写还行,但如果有招聘、房产、二手、二手车、黄页…等多个大部分是共性数据,少部分是个性数据的列表页,每次都这么获取数据,就略显低效了,有大量冗余、重复、每次必写的代码。
特别的,不同业务上游列表页都依赖于底层若干相同服务:
-
一旦一个服务RPC接口有稍许变化,所有上游的系统都需要升级修改
-
子系统之间很可能出现代码拷贝
-
一旦拷贝代码,出现一个bug,多个子系统都需要升级修改
如何让数据的获取更加高效快捷呢?
业务服务化,通用业务服务层的抽象势在必行。
通过抽象通用业务服务层,例如58同城“通用列表服务”:
-
web-server层,可以通过RPC接口,像调用本地函数一样,调用通用业务service,一次性获取所有通用数据
-
通用业务service,也可以通过多次调用基础数据service提供的RPC接口,分别获取数据,底层数据获取的复杂性,全都屏蔽在了此处
是不是连接关系也看起来更清晰?
这样的好处是:
-
复杂的从基础服务获取数据代码,只有在通用业务service处写了一次,没有代码拷贝
-
底层基础数据service接口发生变化,只有通用业务service一处需要升级修改
-
如果有bug,不管是底层基础数据service的bug,还是通用业务service的bug,都只有一处需要升级修改
-
业务web-server获取数据更便捷,获取所有数据,只需一个RPC接口调用
结论:
当业务越来越复杂,垂直拆分的系统越来越多,基础数据服务越来越多,底层数据获取复杂性成为通用痛点的时候,就应该抽象出通用业务服务,简化数据获取过程,提高数据获取效率,向上游屏蔽底层的复杂性。
最后再强调两点:
-
是否需要抽象通用业务服务,和业务复杂性,以及业务发展阶段有关,不可一概而论
-
需要抽象什么通用业务服务,和具体业务相关
任何脱离业务的架构设计,都是耍流氓。
互联网分层架构,为啥要前后端分离?
通用业务服务化之后,系统的典型后端结构如上:
-
web-server通过RPC接口,从通用业务服务获取数据
-
biz-service通过RPC接口,从多个基础数据service获取数据
-
基础数据service通过DAO,从独立db/cache获取数据
-
db/cache存储数据
随着时间的推移,系统架构并不会一成不变,业务越来越复杂,改版越来越多,此时web-server层虽然使用了MVC架构,但以下诸多痛点是否似曾相识?
-
产品追求绚丽的效果,并对设备兼容性要求高,这些需求不断折磨着使用MVC的Java工程师们(本文以Java举例)
-
不管是PC,还是手机H5,还是APP,应用前端展现的变化频率远远大于后端逻辑的变化频率(感谢那些喜欢做改版的产品经理),改velocity模版并不是Java工程师喜欢和擅长的工作
此时,为了缓解这些问题,一般会成立单独的前端FE部门,来负责交互与展现的研发,其职责与后端Java工程师分离开,但痛点依然没有完全解决:
-
一点点展现的改动,需要Java工程师们重新编译,打包,上线,重启tomcat,效率极低
-
原先Java工程师负责所有MVC的研发工作,现在分为Java和FE两块,需要等前端和后端都完成研发,才能一起调试整体效果,不仅增加了沟通成本,任何一块出问题,都可能导致项目延期
更具体的,看一个这样的例子,最开始产品只有PC版本,此时其系统分层架构如下:
客户端,web-server,service,非常清晰。
随着业务的发展,产品需要新增Mobile版本,Mobile版本和PC版本大部分业务逻辑都一样,唯一的区别是屏幕比较小:
-
信息展现的条数会比较少,即调用service服务时,传入的参数会不一样
-
产品功能会比较少,大部分service的调用一样,少数service不需要调用
-
展现,交互会有所区别
由于工期较紧,Mobile版本的web-server一般怎么来呢?
没错,把PC版本的工程拷贝一份,然后再做小量的修改:
-
service调用的参数有些变化
-
大部分service的调用一样,少数service的调用去掉
-
修改展现,交互相关的代码
业务继续发展,产品又需要新增APP版本,APP版本和Mobile版本业务逻辑完全相同,唯一的区别是:
-
Mobile版本返回html格式的数据,APP版本返回json格式的数据,然后进行本地渲染
由于工期较紧,APP版本的web-server一般怎么来呢?
没错,把Mobile版本的工程拷贝一份,然后再做小量的修改:
-
把拼装html数据的代码,修改为拼装json数据
这么迭代,演化,发展,架构会变成这个样子:
-
端,是PC,Mobile,APP
-
web-server接入,是PC站,M站,APP站
-
服务层,通用的业务服务,以及基础数据服务
这个架构图中的依赖关系是不是看上去很别扭?
-
端到web-server之间连接关系很清晰
-
web-server与service之间的连接关系变成了蜘蛛网
PC/H5/APP的web-server层大部分业务是相同的,只有少数的逻辑/展现/交互不一样:
-
一旦一个服务RPC接口有稍许变化,所有web-server系统都需要升级修改
-
web-server之间存在大量代码拷贝
-
一旦拷贝代码,出现一个bug,多个子系统都需要升级修改
如何让数据的获取更加高效快捷,如何让数据生产与数据展现解耦分离呢?
前后端分离的分层抽象势在必行。
通过前后端分离分层抽象:
-
站点展示层,node.js,负责数据的展现与交互,由FE维护
-
站点数据层,web-server,负责业务逻辑与json数据接口的提供,由Java工程师维护
这样的好处是:
-
复杂的业务逻辑与数据生成,只有在站点数据层处写了一次,没有代码拷贝
-
底层service接口发生变化,只有站点数据层一处需要升级修改
-
底层service如果有bug,只有站点数据层一处需要升级修改
-
站点展现层可以根据产品的不同形态,传入不同的参数,调用不同的站点数据层接口
除此之外:
-
产品追求绚丽的效果,并对设备兼容性要求高,不再困扰Java工程师,由更专业的FE对接
-
一点点展现的改动,不再需要Java工程师们重新编译,打包,上线,重启tomcat
-
约定好json接口后,Java和FE分开开发,FE可以用mock的接口自测,不再等待一起联调
结论:
当业务越来越复杂,端上的产品越来越多,展现层的变化越来越快越来越多,站点层存在大量代码拷贝,数据获取复杂性成为通用痛点的时候,就应该进行前后端分离分层抽象,简化数据获取过程,提高数据获取效率,向上游屏蔽底层的复杂性。
最后再强调两点:
-
是否需要前后端分离,和业务复杂性,以及业务发展阶段有关,不可一概而论
-
本文强调的前后端分离的思路,实际情况下有多种实现方式,文章并没有透彻展开实现细节
任何脱离业务的架构设计,都是耍流氓。
思路比细节重要。
阅读前序文章,“分层架构设计”的背景与来龙去脉更加清晰: