1 概述
《微服务设计》,从宏观的角度描述了如何设计整个微服务架构,并谈到了作者自己踩过的很多坑,非常有意思
2 微服务
2.1 什么是微服务
- 按照业务来划分服务
- 每个服务都是自治的,独立开发,独立测试,独立部署
2.2 微服务的好处
- 技术异构性,每个服务可以根据业务的需求来确定自己的实现技术
- 弹性,每个服务就是一个舱壁,能够很好地处理服务不可用和功能降级的问题
- 扩展,可以单独地某个服务进行独立扩展,从而提高性能
- 简化部署,降低每次都要全部部署的风险,每次只部署一部分
- 与组织结构相匹配,每个部门负责自己的服务,避免代码太大难以维护
- 可组合,服务是微小的,能通过不同的组合来实现不同的业务
- 对可替代性的优化,独立升级和更新部分服务
SOA可以算是微服务的其中一个实现方式
2.3 微服务的竞争类技术
- 共享库,将代码共用部分放入到公共库中,缺点是无法异构
- 模块,使用诸如OSGI技术来实现单进程中将异构技术混合起来,缺点是太复杂,而且没有可靠的舱壁隔离。
2.4 微服务的弊端
微服务的弊端,分布式系统的复杂性
- 多服务同时运维的复杂性
- 多服务协同数据处理的复杂性,分布式事务
3 演化式架构师
3.1 架构师的定位
架构师与建筑师最大的不同是,软件是不断随着需求进行演化的。架构师更像是一个城市规划师,他的主要职责是
- 避免一开始追求完美的架构
- 建造一个合理的可扩展架构来适应需求的变化
- 关注服务之间的交互,而避免关注服务内部的事情
架构师应该建立目标,原则,与实践来约束整个系统往恰当的方向前进
3.2 架构目标
- 监控,能清晰地看出整个微服务的健康状况
- 接口,使用标准的方式来进行微服务的沟通,从而减少团队间的学习成本
- 安全,避免微服务之间的崩溃传递
3.3 代码治理
- 范例,正确的可模仿代码
- 服务代码模板,实现服务的常用代码,一般会被写成共享库,但要注意避免将太多东西写入共享库,造成系统过度耦合
4 如何建模服务
建立微服务的第一步是,确定服务的边界
4.1 什么是好的服务
什么样的服务是好服务
- 松耦合,修改一个服务就不需要修改另一个服务
- 高内聚,相关行为聚集在一个服务,一个功能在一个服务
4.2 限界上下文
- 共享的隐藏模型,避免将服务内部的模型暴露出去,这样会导致服务内部变化时,接口也会变化,造成系统紧耦合
- 模块与服务,让服务边界与领域边界保持一致
- 过早划分,避免过早划分领域,而要应该在服务稳定后才逐渐拆分成微服务
边界是可以嵌套的,库存服务是一个边界,而库存下面则可以划分多个服务出来
避免通过技术来划分服务,例如渲染层服务,逻辑层服务,数据层服务,这样会导致严重的分层与紧耦合。
5 集成
建立微服务的第二步是,确定服务之间的沟通方式
5.1 通信方式
- 避免破坏性修改,服务新增字段后原来的消费者不受影响
- 保证API的技术无关性,可以无缝地让其他语言,框架,工具也能接入通信中
- 服务易于消费方使用,客户端能在不需要安装第三方库的情况能就能调用服务
- 隐藏内部实现细节,避免需要暴露内部实现的技术
5.2 同步通信
创建服务接口时,避免使用CURD的数据库方式,这样会造成严重的耦合
通信方式需要支持同步与异步的两种方式,使用同步还是异步
- 同步调用比较简单,容易指导整个流程的工作是否正常,但问题是容易造成下游服务的沦为CURD的贫血模型
- 异步调用麻烦一点,但是能将整个流程的各个模块切开耦合,使用修改起来更加容易,但问题是当其中一个流程出错时,很难找出问题的地方
同步调用的两种实现方式,RPC与REST
RPC,远程过程调用,优点:
- 自动生成客户端的桩代码,调用方便
缺点:
- 技术的耦合,Java RMI与Java平台强烈绑定
- 过分隐藏远程调用,远程调用中网络是不可靠的,RPC缺少对网络中不同错误的处理
- 接口使用脆弱,Java RMI在增减服务器字段时消费者需要重新部署
结论是,使用RPC需要优先使用诸如protocol buffers或Thrift这类现代化跨平台的RPC实现,而不是RMI这类的RPC。
REST,构建在Http上的同步调用,优点:
- Http上强大的生态系统,Varnish的缓存,mod_proxy的负载均衡,http的流量监控
- 超媒体作为程序状态引擎,使用与人类阅读相似从该链接发现其他链接的方法,能有效地解耦调用的url,但这样做会造成过多的网络访问
- 可以根据客户端的需要返回JSON,XML或其他的格式的数据
- 避免将数据库表暴露为服务的接口,这样启动足够快,但会暴露内部实现,导致实现迭代困难
缺点:
- 无法自动生成客户端的桩代码,调用麻烦
- 部分Web框架无法支持所有的Http动词,这个基本没有大问题
- 基于Http上的协议性能不强,在要求低延迟的环境,Http庞大的通信包是个问题
5.3 异步通信
异步调用的两种实现方式,消息代理与ATOM
- 消息代理,使用传统的RabbitMQ来实现订阅发布模型,简单直接,要注意的是避免消息代理承载太多的功能
- 基于Http的Atom协议,需要自己跟踪消息是否到达,以及管理轮询,使用起来比较复杂
异步调用的复杂性,需要做好消费方的监控,避免消费方无限次的失败重试导致故障转移的问题。
5.4 一些细节
- 避免将公共领域对象放入公共库中,这会导致生产者与消费者之间产生耦合
- 避免过重的客户端库,客户端库应该只包含服务发现,故障模式,日志等方面的工作,不要与具体的业务相耦合
- 按引用访问对象,能避免对象过期的问题,但也导致了耦合了对象的访问层
5.5 版本管理
微服务升级时,怎么做版本管理
- 选择正确的技术,避免数据库修改影响接口的修改
- 客户端的容错性读取,当返回数据增减部分字段后,客户端能够容错
- 保留旧版本的接口,让旧服务走旧接口,新服务走新接口
5.6 用户界面的集成
为了应对不同终端的UI,怎样设计微服务
- 提供细粒度的数据API,Web终端与手机终端通过统一的API获取数据,然后来拼凑属于自己的UI。优点是一套API解决全部问题,缺点是同一API下不同终端所需的工作是不一样的(Web端与app端发布食谱的积分不同),而且API的通信过多,导致手机终端的发热严重
- 提供粗粒度的UI片段API,API吐出的不是数据,是UI片段,各个终端根据需要将UI片段拼凑起来即可。优点是,减少了API通信过多的问题,缺点是原生应用中无法消费这些UI片段
- 建立一个API聚合网关来提供服务,优点是解决了API通信过多,以及细粒度API变化过多的问题,缺点是API聚合层太厚,需要处理不同终端下的逻辑
- 建立一个终端相关的API聚合网管来提供服务,根据不同的终端建立不同的API聚合层,优点是解决了API通信过多,细粒度API变化过多,以及聚合层API变化过多的问题
5.7 第三方软件的集成
由于部分原因,需要与遗留的第三方软件集成在一起,而这些第三方软件,语言与工具是不受控制的,并且会提供一些私有的API规范来提供定制
- 意大利面式集成,直接调用它们的API来集成,很有可能会耦合到它们技术中
- 外观模式集成,将它们的API封装起来,然后对外保持标准的通信,对内使用它们的通信方式
- 绞杀者模式集成,在原有系统中拦截API请求,将这些请求逐渐迁移到新的微服务上
6 分解单块系统
6.1 为什么要分解单块系统
为什么要分解单块系统
- 更快的后期开发速度
- 让技术架构匹配团队架构
- 让数据敏感的地方圈在一个地方,增强安全性
- 让独立的模块选用最适合的技术
分解单块系统的关键是,找出系统中的接缝,然后把它分解出来。接缝就是相对独立的一部分代码,对这部分代码进行修改不会影响系统的其他部分。
6.2 数据库问题
分解遇到的第一个问题是,数据库
- 外键约束,分解系统后,两个跨服务的表就需要去掉外键约束,然后使用跨服务的一致性检查,或周期性的清理任务来让两个表的数据达成同步。
- 共享静态数据,分解系统后,如何共享像国家编码的这种静态数据,要么维护一份共享配置表,要么维护一份共享代码枚举,避免将这些数据抽象成一个公用服务,这样的话复杂性太高了。
- 共享动态数据,分解系统后,像客户数据这种动态数据,无论是库存服务和订单服务都是需要的,该怎么共享。解决办法是将客户数据抽象成一个公共的客户服务。
- 共享表,分解系统后,原来两个服务都共用的一个表,就将其拆分为两个表,通过服务接口来获取数据
6.3 事务问题
在原来的单块系统中,订单插入与库存插入是一个事务操作,那在拆分以后应该保证这个事务操作
- 再试一次,依赖于消息中间件的重试操作,数据为最终一致性
- 终止整个操作,依赖于消息中间件的回退操作,数据为最终一致性
- 分布式事务,依赖于事务管理器来保证跨服务的事务性,数据为强一致性
- 本地事务,将分布式事务转换为本地事务操作,数据为强一致性
6.4 报告问题
报告系统,就是那些需要汇聚几个表来做聚合数据输出的系统,例如,统计每日的销量与类目分布情况
在单块系统中,为了避免报告系统对主系统性能的影响,一般会将单块系统的数据库周期性地导入到另外一个只读数据库中,让报告系统从只读数据库中生成报告。
在分布式的微服务中,有什么方案呢
- 服务调用,报告系统通过服务调用批量获取订单与库存系统的信息,然后通过整合信息来生成报告。优点是简单,缺点是,影响主系统性能
- 数据导出,订单系统与库存系统的数据库定时同步到报告系统的副本中,报告系统通过在副本数据库中生成视图来优化查询。优点是速度快,集成简单,缺点是造成存储层耦合。
- 事件数据导出,订单系统与库存系统在新增或修改数据时,发布事件数据,报告系统收到数据后将其修容到自己的数据库中。优点是速度快,无数据库耦合,缺点是集成复杂
6.5 实施
实施分解的步骤为:
- 先分离数据库结构
- 再分离服务
7 部署
7.1 持续集成
持续集成的好处
- 保证代码可构建,提交代码后,CI后尝试根据提交的代码来生成一个构建物,Java是jar包,C++是so文件等等,如果编译失败,CI会阻止这次的代码提交
- 保证代码经过测试,CI生成构建物后,就会进行对应的自动化用例测试,以保证每次提交的代码都是经过单元测试的
- 建立代码与部署物的映射,最终部署时,CI是提供测试用的构建物来部署,当线上的构建物出现问题时,我们能随时回溯到构建物所在的源码版本
微服务集成CI的方案
- 一个CI和所有微服务,每个微服务提交后都进行一次整体的CI流程,优点是简单,缺点是大大加长了CI周期的流程,每次的构建物都是一整个系统
- 一个CI和一个微服务,每个微服务都有属于自己独立的CI流程,优点是构建流程缩短,每次的构建物仅仅是一个服务,缺点是繁琐
7.2 构建物
生成的构建物应该是什么,如果是基于技术栈的构建物体,Java是Jar包和War包,Python包是egg与Nginx,Supervisor等工具,就会造成部署过于复杂,不同技术栈下的构建物需要安装与配置不同的东西,开发与测试想部署其中一个微服务时就很麻烦
解决方案有:
- 基于OS的构建物,将构建物统一生成基于OS的构建物,例如RedHat是RPM包,Ubuntu是deb包,Windows是MSI包,这样不同技术栈就能透明成一个安装包,需要部署时都是统一的安装方式。优点是简单,缺点是不同跨OS来部署
- 基于虚拟机的构建物,先生成一个统一的依赖工具包,如Apache,collectd等等,然后将构建物打包到镜像中,部署时将镜像直接运行就可以了。优点是跨OS部署,而且安装快,不需要再查找安装依赖了,缺点是包很大(20g)
- 基于容器的构建物,跟虚拟机一样,只是将虚拟机替换变成容器,这样的话就能大大减少容器包的大小,但要注意的是,构建物依然很大,高达500m左右
构建物的理想方法是变成不可变服务,开发人员不能直接SSH到具体机器上发布或修改配置,直接通过提交CI,然后CI生成构建物来发布,这样部署过程变得简单,而且更容易定位问题(每次修改都有痕迹)
7.3 配置
不同环境下的配置是不一样的,怎样解决
- 不同配置不同构建物,提交CI后同一份代码生成多份构建物,然后用test构建物测试,prod构建物发布。优点是简单,缺点是速度慢,而且测试构建物与发布构建物不一致,无法保证CI的准确性
- 不同配置同一构建物,将配置抽象出来,放在配置文件中,在启动构建物时指向配置文件,当然这个配置也可以是放在中央的配置系统中。优点是快速,构建物准确,缺点是麻烦
7.4 主机与服务
最后,部署服务时,服务与主机之间应该如何映射呢
- 单主机多服务,优点是简单,缺点是监控服务困难,并且同一主机内的服务会相互影响,还会影响单个服务的扩展性,所以严重不推荐。
- 单主机单服务,优点是监控容易,服务隔离,缺点是需要管理众多的机器
- 基于Paas的服务,将服务部署到Paas,Paas能有效简化管理单个服务下的多个机器,优点是监控容易,服务隔离,管理容易,缺点是需要应用程序与Paas进行技术耦合
8 测试
8.1 测试范围
测试金字塔,描述了自动化测试类型分为三种,分别为单元测试,服务测试和端到端的测试,这三种测试从左到右,测试更快,隔离更好,从右到左,范围更大,更有信心。
好的经验法则是,下一层的测试数量要比上一层的测试数量多一个数量级。让测试在覆盖性与效率之间取到一个平衡。
一个比较糟糕的场景是,4000个单元测试,1000个服务测试,60个端到端测试,服务测试和端到端的测试太多造成测试的总时间太长,反馈周期太长。
更为极端的做法是,只有端到端的测试,没有单元和服务测试,造成测试运行起来极其缓慢,而且脆弱,团队也越来越不愿意做测试了。
8.2 单元测试
单元测试,就是只测试一个无副作用的函数和方法调用,这个函数和方法内部不会有副作用,而且也不会调用其他外部的服务。
这样的测试只依赖于输入的数据,所以测试速度非常快,但是测试的范围也是最少的。
8.3 服务测试
服务测试,就是测试服务的各个接口,如果这个服务依赖其他外部服务,那外部服务都是要打桩或者mock的,这样能将测试隔离在这个服务内部。
隔离外部服务的两个方法
- 打桩,对外部服务建立一个桩对象,指定其每次都返回一个预设响应的结果。例如,10001输入就是返回1,10002输入就是返回2。
- mock,对外部服务建立一个mock对象,相当于桩对象更进一步,其会根据不同的调用次数返回不同的结果。例如,第一次输入就是返回1,第二次输入就是返回2。
可以看出,mock是增强版本的桩实现,其会考虑不同调用次数下的返回结果。所以,mock的方式更强大,也会更脆弱,当服务实现改变了,调用mock对象的次数更改了,mock测试就会失败。但打桩就没有这个问题。所以,绝大多数情况下,我们应该考虑打桩,而不是mock。
8.4 端到端的测试
端到端的测试,就是放开整个服务链的mock,将整个系统的服务部署起来,对一些核心场景进行整体的端到端测试。端到端测试的范围更大,信心更强,同时也是最慢最脆弱的。
- 太脆弱,端到端的测试需要联通极多的服务,其中一个服务出现少许出错时,都会让整个测试失败,这会导致异常正常化,测试人员越来越不信任测试的失败结果。
- 太缓慢,完整的跑一次端到端的测试,会耗费过多的时间,导致测试不能每次提交都进行一次,快速测试成为一个幻想
- 修复周期太长,由于脆弱性与缓慢性,导致无有意义的bug反馈太多,最终测试人员也都不信任测试的结果了
所以,端到端测试的测试最好不是针对功能,而是针对核心的一两个场景进行的,少而且重要。
作为端到端测试的替代方案,我们可以考虑消费者驱动的契约测试(CDC)。由服务消费者编写测试用例,服务生产者的下游进行打桩,这样就能改善脆弱性,同时也比面向服务测试来得更为细致。
8.5 部署后再测试
最后是,在敏捷开发中提倡的部署后再测试
- 蓝绿部署,在部署时,将新版本部署到单独的机器中,然后在新版本上执行冒烟测试,冒烟测试成功后才让线上流量导向到新版本中
- 金丝雀部署,在部署时,将新版本部署到单独的机器中,然后线上的流量会复制一部分到新版本上,观察新版本的返回结果以及状态码来确定新版本是否有问题。优点是更可靠,缺点是对有副作用的服务是有问题的,而且部署的路由与资源都复杂很多。
8.6 跨功能测试
跨功能测试,就是那些业务功能以外的测试,例如是容错性测试,性能测试,可用性测试等等。
9 监控
9.1 问题
监控的演化
- 单一服务和单一服务器,监控CPU,内存,查看日志时就直接远程登录就可以了
- 单一服务和多个服务器,使用Nagios来聚合监控主机和应用程序数据,查看日志就需要多开窗口来同时查看
- 多个服务和多个服务器,海量的日志无法做多窗口来查看,主机和应用程序数据如何做聚合查询
9.1 收集框架
针对以上问题,建立一个针对性的多服务监控系统
- 使用zabbix或者Nagios来收集主机运行数据,CPU,内存等等
- 使用logstash统一收集应用日志
- 使用Kibana来做日志的聚合实时查询
- 使用Graphite来做日志的汇总报告系统,显示一段时间内的数据走向
这样就能同时实现聚合监控主机与应用程序指标,以及查看日志的功能。
9.2 数据指标
应用程序指标
- 本服务的响应时间和错误率
- 下游服务的响应时间和错误率,这样能避免只监控本服务的响应时间和错误率会错过网络错误与超时导致的级联错误
- 定时运行的语义监控,预先配置好的线上测试,将测试结果上传
日志的写法
- 关联标识,每行日志应该包含关联标识,这样能从一个请求回溯到所有的调用关系链
- 统一的日志格式,提供公共库来简化日志的输出方式
9.4 其他
分布式监控系统,目前的趋向于大统一的状态,统一的数据收集,和不同的数据处理流向相结合的方式。
10 安全
10.1 服务外部安全
服务与外部通信时需要身份授权,怎么办
- 粗粒度授权,提供单点登录网关,主体访问微服务前都需要经过网关,网关验证主体身份,如果验证不通过,就会跳转到登录页面,验证通过就将身份信息一起传递到后面的微服务中。这样做避免了每个微服务的重复的身份验证。
- 细粒度授权,微服务的不同接口有业务需要的授权,例如,10001用户能访问所有库存,但其他用户只能访问自己的库存信息。这样应该放到服务内部去校验,而不是放到网关内完成。
10.2 服务内部安全
服务间通信时需要身份授权,怎么办
- 边界内允许一切,这相当于隐式信任模型,优点是简单,缺点是一个被攻击就等于全部被攻击。
- SSL验证,将SSL证书打包到服务中,服务通信时需要SSL验证,优点是简单,缺点是吊销证书时会很麻烦
- hmac验证,使用hmac将请求数据加密然后发送给服务方,优点是更简单,而且更换hmac码较为容易,缺点是无法识别调用方。
- api密匙,使用api key与api secert将请求加密后发送给服务方,优点是简单,容易更换,而且能识别调用方,缺点是缺乏统一标准。
10.3 静态数据安全
像保存到数据库的数据,如何保证它们的安全性
- 使用众所周知的加密算法进行数据加密
- 加密密匙需要放在单独的安全设备,或者使用数据库的内置加密支持
- 细粒度加密,避免对整个数据库进行加密,只对服务内部的部分敏感数据进行加密
- 备份加密,别忘了备份也是需要加密的
10.4 防御系统
防御性的加密措施
- 防火墙
- 日志监控
- 入侵检测与预防系统
- 网络隔离
- 操作系统补丁
11 康威定律和系统设计
11.1 康威定律
任何组织在设计一套系统时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。
所以,让组织倾向于模块化,能让系统结构更加灵活,清晰。
12 规模化微服务
12.1 容错
分布式环境中,错误无处不在,如何拥抱错误,当失败时仍然能让系统正常工作
- 故障发现,从架构上来发现下游服务的故障,并且保证下游服务的故障不会对上游服务造成级联传递。
- 故障处理,从业务上来解决当下游服务故障时,如何优雅地处理。
故障发现
- 超时,所有调用下游服务的请求都必须带上超时时间,而不是让无限等待下游服务的返回。
- 断路器,就像雷电来了,家里断路器断开,然后保护了全家的家电一样。上游服务如果发现下游服务在短时间内有大量失败返回时,直接断开下游服务,对下游服务的所有请求进入快速失败模式。直到检测到下游服务正常后才重新开启下游服务。
- 舱壁,就像船里面的舱壁保证了一处漏水不会造成全船漏水一样。每个下游服务的请求都使用一个单独的连接池,避免单个下游服务过多的崩溃导致其他的下游服务无法调用。
- 隔离,将系统划分更多更细粒度的服务,将故障隔离控制到更小的粒度上。另外,也要减少服务之间的依赖,避免服务故障不断传递。
故障处理
- 功能降级,当服务检测到下游服务失败外,做恰当的降级服务处理。例如,购物车服务发现库存服务崩溃后,不应该阻止全站运行,而应该继续提供购物车服务,只是提醒用户暂时不可以下单而已。
- 幂等重试,将失败的下游服务放入队列中,队列定时会重试调用这个下游服务。为此,下游服务必须支持幂等性质,同一请求参数不能产生多次的副作用。
12.2 服务扩展
服务由于业务发展的需要扩展
- 更多的容错性,更多的机器避免单点故障
- 更好的性能,更多的机器提供并发性能
服务扩展的方式
- 垂直扩展,买买买,提升服务器配置,更好更稳定的硬件设备。
- 拆分服务,将服务一拆为多,让关键服务与一般服务切分出来,避免一般服务故障影响到关键服务上。
- 水平扩展,将服务所在的机器分为多个,这样提供更好的并发性能,与更好的容错能力。
水平扩展的实现方式
- 负载均衡,多个服务前面套一个负载均衡器,当其中一台机器故障时,负载均衡器就会自动摘掉它。优点是透明简单,缺点是无法实现资源最大化。
- 队列worker,多个服务作为worker处理一个队列的任务,调用方将任务放入队列来处理。优点是简单,资源最大化,缺点是调用方式只能是异步的。
12.3 数据库扩展
服务是无状态的,也是最容易被扩展的,可是数据库呢,如何扩展
- 扩展读操作,通过数据库的读写分离方式,提高数据库的读性能。
- 扩展写操作,将数据分片,放到不同机器的数据库,这样能同时提高数据库的读和写的性能。但主要问题是,分片对数据查询很麻烦,扩展节点时也需要数据迁移。
- 拆分数据库,让每个微服务独享一个数据库,而不是让所有微服务共享一个数据库,这样就能大大提高数据库的读写性能,但会遇到分布式事务的问题。
- CQRS,将数据库的读写分离模式扩展到服务上来做,让读和写都达到了最大的效率。
12.4 CQRS
CQRS的架构非常有趣,可以特地说一下
- 整个系统的操作被划分为两方面,读操作与写操作,读操作为Query,写操作为Command。
- 写操作时,写操作将参数封装成一个对象扔入队列,然后直接返回用户正在处理中。
- 写操作的领域模型,接收队列请求,然后将请求划分为多个细粒度的请求,然后再次请求底层的领域模型来处理。注意,处理队列请求时,如果其中一个步骤失败了,队列请求会执行幂等重试的机制,这保证了所有请求最终都会执行,数据会达成最终一致性。
- 写操作的细粒度领域模型,会接收请求,然后根据请求的内容修改数据库,注意,如果领域模型是多机模型的,请求会根据Id路由,保证同一Id只会路由到同一机器上的领域模型上。然后,本机的领域模型在内存时做排队处理,同一Id的请求排队处理,不同Id的请求并发处理。
- 写操作同步,在写操作的所有操作都完成了以后,细粒度的修改操作会合并成多个事件抛给另外一个领域模型,另外一个领域模型修改读操作的数据模型
- 读操作执行,读操作就是直接读取读操作的数据模型,注意,这里读操作的数据模型都是针对Query请求进行优化的,所以所有的Query操作都是类似select * from xxx where id = xxx的操作,速度超快。而且读操作的数据模型可以是缓存等数据结构。
所以,CQRS的架构会有以下的优点:
- 无数据事务性,跨服务的数据一致性由队列的幂等补偿机制来保证,数据都是最终一致性的。而且,这个机制对开发者来说,几近透明!任一命令在处理过程中崩溃或者失败,数据都会自动回滚到最终一致状态。
- 无数据竞争性,服务内的数据会根据Id路由,以及内存排队,服务并发处理的数据都保证了是Id不一样的数据,数据库根本不需要加锁!
- 无数据联表操作,由于读模型都是针对Query来设计的,每个读操作都是简单的select操作,根本不需要联表。
- 读写分离异构,读写的数据模型完全没有耦合,你可以以各自最优的方式来实现双方的数据模型。
- 写操作回溯,所有的写操作都是在Command传递进来的,所以通过序列化所有的Command就能回溯用户的所有请求,方便问题排查。
对于需要强事务性,高并发读的业务来说,CQRS几乎完美!但是,CQRS也有以下的明显缺点:
- 写操作反馈延迟,所有写操作都是异步,返回速度快,但就是用户感觉就是点了后没有反应。
- 节点伸缩困难,由于写操作是由Id路由的,以保证同Id同机器,所以,当机器增多或者减少时,路由表需要做艰难地适应,才能保证数据是没有竞争的。
- 架构复杂难以理解,你无法想象一个CURD都要写很长很长的代码。
可是,无法否认,CQRS架构带给了我们另类的思考
- 通过队列和路由表,我们就能实现跨服务的无数据竞争的事务处理。
- 通过事件同步,我们就能读写分离,最优化读写的数据模型与结构。
因此,我们可以构造这样的微服务,利用CQRS的优化,而规避CQRS的缺点。
- 接口的写操作可以用注解的方式表明开启跨服务的数据一致性,而数据竞争则由数据库的事务来保证。
- 事件操作,写操作完成后再同步更新所有的读数据模型
- 接口的读操作直接将读操作的数据模型取出来即可。
12.5 缓存
缓存是性能优化的常用手法,要注意如下
- 缓存可以使用客户端,代理和服务器缓存三种,具体使用什么缓存是需要与任务相绑定的。例如缓存图片文件用客户端缓存,缓存html文件用服务器缓存
- Http缓存,http有多个缓存字段来协助缓存,注意,etag字段的缓存用法
- 写缓存,如果用大并发的写请求,可以使用后写式的缓存优化
- 故障缓存,为可能故障时提供的缓存,例如定时备份整个网站,当网站故障时,代理会指向备份的缓存区域
- 隐藏源服务,缓存消失时尽量避免直接访问源服务,这样会导致源服务大量回流崩溃。缓存消失时应该快速返回失败,然后抛出事件,让源服务更新缓存。
- 保持简单,过多的缓存会难以评估数据的新鲜程度,需要尽可能少用缓存
- 缓存更新,客户端和代理缓存文件后,如果你需要更新,就只能更新url了。
12.6 服务发现与注册
当服务很多时,怎么让服务发现对方的位置,这就是服务发现与注册的问题
- DNS,使用DNS来发现服务的IP地址。域名可以用”服务名.环境.域名后缀”来确定,优点是标准,简单,缺点是更新不及时。使用DNS方案时最好将DNS指向负载均衡,让负载均衡器调度服务实例的增加或减少。
- 动态服务注册,使用诸如Zookeeper,Consul和Eureka等分布式注册中心,优点是可靠,更新快,缺点是需要额外加库。
12.7 文档服务
维护服务的API文档
- Swagger,支持多语言的服务API描述文档
- HAL,除了有Swagger的文档,还能在线调用服务
- UDDI,记录组织中有关服务的信息
13 总结
这本书非常好,从集成,开发,部署,测试,安全到团队结构探讨了整个微服务的实现方案,并提供了不同的方案来比较其中的优劣点,获益良多。
目前,微服务的主要发展发向有两个
- 服务治理框架,着重于服务间的简单调用,可靠,监控的,淘宝的Dubbo,微博的Agenda
- 服务跨平台框架,着重于服务间的跨平台调用,Google的gRPC,Facebook的thrift。
前者在框架层面已经实现了完整的服务调用,监控,以及容错处理,使用时只需要填入业务代码就可以了,但缺点也很明显,其与某一类技术给绑定了。后者则专注于服务的跨平台跨语言的调用,监控,容错的工作一部分需要扔给proxy来做,而另外一部分则需要不同语言下的公共库来处理,所以使用起来也麻烦一点,但优点就是可以随意选择你想用的语言,以及随意升级你想用的框架。
所以,在技术初期可以考虑成熟的服务治理框架,在技术后段,在有成熟的框架开发技术时,则可以自己构建proxy来搭建自己真正的微服务框架。
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!