1. 概述
我们需要科学的学习方法,从一个技术出现的背景,解决的问题、优势和局限去思考,从技术的本质来理解,才不至于深陷各种眼花缭乱名词中。
复杂的应用架构不是一蹴而就的,是不断的演化而来的。
在业务的发展初期,系统功能相对初级,三五个人,十来条枪,基础设施薄弱,甚至都不配备专业的运维,追求的是简单、高效,快速迭代和上线。这个时候往往采用单体式架构,一个war包或者fat jar包含所有功能和代码(前后端分离是另外一个故事,不在本文考虑范围内)。
1.1 单体架构
所有代码都在一个工程内,原型验证阶段往往采用这种架构,适用前提:
- 流量小,一个节点就足够应付
- 业务逻辑简单,单个人能理解全局
- 员工数量少,同时修改冲突少
原型验证初见成效时,单个后端节点可能不足以支撑用户快速响应的要求,但是有不想花太高的成本,负载均衡和集群是这个时候最自然而然的选择
1.2 集群和负载均衡
所有代码仍然在一个工程内,不带来额外的开发工作,只是引入反向代理做负载均衡。适用常见原型验证初见成效的时候。很多互联网公司后台管理系统可能也会停留在这个节点,他不会有很大PV,对QPS和响应时间也没有多大要求。
引入了负载均衡后,如果后端是无状态的,配合DNS解析和Nginx反向代理,理论上后端能支撑无限大的流量,支撑能力不足时只需要简单的增加Nginx、Fat-Jar的部署即可。
现实世界中,后端几乎不可能是无状态的。流量增长到一定阶段,系统的瓶颈会变成后端对应的状态存储,比如数据库。要想提高数据库的扩展能力,主流的方法是缓存、读写分离、垂直拆分、水平拆分。难度依次递进。缓存和读写在单体架构阶段就能引入,能帮助我们支撑一定的流量。早早晚晚,最终我们会走到数据库的垂直拆分,并伴随着部分大表的水平拆分。
1.2 垂直拆分
数据库垂直拆分后,数据库QPS瓶颈就解决了,随着后端节点的增加,每个后端节点都需要和每个拆分后的数据库建立连接,数据库连接会成为新的瓶颈。
这个时候,业务可能已经复杂到一定程度了,开发团队规模开始增长,新员工理解完整的系统层本高昂。
为了解决数据库连接瓶颈、降低开发者对系统的理解层本,会开始将系统划分为不同的子系统,这就是垂直切分。
1.3 SOA
垂直拆分后的系统肯定不是孤立的,比如订单服务,我可能需要知道用户的VIP等级,给予不同的优惠;同时产品服务,也需要知道用户的VIP等级展示不同的价格;等等。导致的新问题是:
- 查询用户VIP等级的逻辑散落在各个子系统中,很难保证一致
- 各个子系统需要了解用户Schema定义,任意升级都需要考虑老代码兼容,如果不兼容,需要关联子系统同时上线。
- 各个子系统在直连用户数据库,用户数据库连接依然不堪重负
通过对外提供UserService的jar包依赖能解决上面的问题1,前提每次发布jar包后关联子系统都升级上线;部分解决问题2,他们不要关心UserService的实现逻辑,但是依然要上线。但是无助于解决问题3。同时自身的逻辑变更都依然其他子系统的升级版本和上线耦合太过严重。
SOA是为了解决上面的问题诞生的。接上面的例子来说,订单服务和产品服务想要获取用户VIP等级,并不直接连接用户数据库,也不引用UserService的jar包,而是通过RPC请求从UserService的服务端获取。
早期的SOA特别简单,可以退化为只提供RPC服务。
这种方式存在明显的问题是UserService单点,在高可用的系统里边是不可接受的。于是出现了改进版:
通过Nginx转发一定程度上实现了高可用,Nginx基于心跳剔除不可用的后端节点。早期并没有OpenResty的存在,多数公司也没有基于Nginx二次开发的能力,所以每添加一个UserService节点都伴随着一次手动修改Nginx配置的操作,对运维造成层本。同时客户端请求经过一次转发,性能略有损耗。
为了解决上面的问题,进一步改进就是现代的SOA框架了,1号店的Hedwig、微博的Motan、阿里的dubbo,无一例外都属于这种:
服务提供者通过将自己注册到服务注册中心(常见的解决方案是Zookeeper、Nacos),客户端调用时,先根据服务名称(可能还有机房、分组、版本号等)信息,找到服务调用地址(服务提供者注册是提供),再根据服务调用地址调用。
服务提供者将自己注册为Zookeeper的临时节点,宕机后临时节点会消息,客户端通过Watch监听Zookeeper临时节点的变化,通过感知变化将不可用的节点从调用列表里摘除。
1.4 微服务
SOA解决了业务重用、资源链接瓶颈的问题,架构师们总有对服务进一步拆分的冲动:
- 解耦,比如用户服务,不希望用户积分影响到用户登录
- 热点独享资源和故障隔离,比如秒杀我们希望给它更多资源,秒杀导致系统崩溃时不影响普通购买
微服务可以理解为SOA的服务拆分粒度的最佳实践。
2. 微服务架构的挑战
2.1 微服务架构的优点
1. 功能重用、逻辑一致性
同一逻辑不需要重复实现,避免了重复代码,也避免了相同逻辑多个实现的不一致。可以通过jar包重用功能,但逻辑变更后会导致关联上线。
2. 资源连接瓶颈
不许系统内的任意系统都连接给定资源(如数据库),而是有给定服务的集群连接资源,其他系统通过调用服务获取数据。
3. 降低复杂度
聚焦,开发者只需要关注部分,而不需要关注全局,降低理解成本。
4. 可扩展性
可以根据业务特点和性能要求,针对特定服务增加集群规模或提升系统硬件配置。
5. 解耦、故障隔离
只要保证接口稳定,系统直接的发布上线就不存在强依赖。独立进程,单个服务的崩溃不会影响全局。
6. 容错、动态扩容
服务集群里的部分节点宕机不影响整体服务,整个服务崩溃通过重试、服务降级依然能对外服务。服务注册与发现。
2.2 微服务架构的挑战
微服务不是银弹,采用微服务的过程中,往往需要我们完成数据库拆分,这时候会带来分布式事务的问题;一个正常的业务被拆分了多个微服务的调用链;大量的服务,对构建、发布、运维都提出了更高的要求。
1. 故障排查
一次请求可能会涉及多个服务的多次调用,而每个服务本身又是一个集群,调用可能是其中任意一个节点,如果通过传统的方式直接到文件查看日志,怎么找这次请求关联的日志会是一个挑战。这也是全链路监控出现的原因。
2. 服务监控
成百上千个微服务如果都是点状式的监控,单纯的将每个服务当一个进程监控,很难直观的查看哪些服务故障,影响范围是多少。服务治理就变得极其的重要,服务的全局依赖图可能对我们有所帮助。
3. 分布式架构本身带来的复杂性
网络通信的延迟和故障,已经不预测的调用失败带来的数据一致性保障问题。
4. 运维成本
运维的本质是保证机器、系统、网络、进程和服务的正常运行,大量拆分的服务,在故障率恒定的前提下,故障的频次会明显增加(这又是一个权衡了,故障次数增加,但是每次故障范围减少)。大量服务的快速部署、统一管理、扩容都对运维提出了更高的要求。