微服务学习笔记
1 微服务
微服务:基于业务领域建模的、可独立发布的服务,把业务内聚的功能封装起来,并通过网络供其他服务访问。
好处:
- 技术异构性,不同服务可以使用不同的技术
- 弹性,可以更好的处理服务不可用的问题
- 扩展,只对需要扩展的服务进行扩展
- 简化部署,各服务的部署相互独立,能更快的部署或回滚单个服务
- 与组织结构相匹配,不同团队维护各自的服务
- 可组合性,服务可重用、可组合
- 可替代性,更容易重写或删除单个服务
演化式架构师:
- 愿景。确保在系统级有一个经过充分沟通的技术愿景,这个愿景应该可以帮助你满足客户和组织的需求。
- 同理心。理解你所做的决定对客户和同事带来的影响。
- 合作。和尽量多的同事进行沟通,从而更好地对愿景进行定义、修订及执行。
- 适应性。确保在你的客户和组织需要的时候调整技术愿景。
- 自治性。在标准化和团队自治之间寻找一个正确的平衡点。
- 治理。确保系统按照技术愿景的要求实现。
服务要做到松耦合、高内聚。
2 集成
什么是好的集成:
- 避免破坏性修改
- 保证APl的技术无关性
- 服务易于消费方使用
- 隐藏内部实现细节
共享数据库:
优点是集成简单,缺点是消费方能够查看实现细节,耦合度高,且与特定的数据库技术绑定。
尽量避免数据库集成。
同步与异步:
- 同步:请求/相应,具体技术有RPC、REST
- 异步:基于事件,也可以基于异步请求加回调
编排与协同:
- 编排:依赖于某个中心大脑来指导并驱动整个流程
- 协同:各自负责自己的职责
优先选择协同方式。
远程过程调用(RPC):
使用本地调用的方式和远程进行交互,使用时要注意以下事项
- 远程调用的性能问题
- 网络本身的不可靠性
- 有些RPC机制会导致技术耦合,如Java RMI限制了必须使用Java
- 应该能独立的升级服务端接口,而不升级客户端
- 目前gRPC凭借高性能和强类型接口被广泛采用
表述性状态转移(REST):
REST是一种网络请求的风格。服务可以根据请求内容创建资源的不同表示形式。比如,客户端请求一个Customer的JSON表示形式,一旦客户端得到了该Customer的表示,就可以发出请求对其进行修改。通常使用HTTP实现REST。对于一个资源,访问接口只有一个,通过HTTP协议的不同动词对其进行不同的操作。
基于事件的异步协作:
有两种技术可选择:
- 采用MQ中间件实现消息的发布订阅
- 使用HTTP来传播事件,可通过ATOM协议提供资源聚合的发布服务。
响应式扩展(Rx):
可以把多个调用组装起来并在此基础上执行操作。这些调用可以是同步的,也可以是异步事件。可对这些被观察者应用map、filter等函数变换。
服务之间的代码重用:
服务之间、或者服务端与客户端的代码重用可能会带来过度耦合。最好只重用底层代码,如服务发现、故障转移、日志等。不要把与目标服务相关的代码放到客户端中。保证服务的独立发布。
按引用访问:
从其他服务获取一个资源时,要明确信息的有效性时限,可以做本地缓存以提高性能,但应保存原始资源的引用,在必要时更新信息。
集成第三方软件:
第三方软件缺乏控制,没有统一的集成方式。可以开发一系列服务专门集成第三方软件,其他服务访问这些中间服务。
3 分解单块系统
步骤:
- 划分服务的边界,将同一服务的功能组织到一起,比如Java可划分为多个package。
- 可使用IDE的重构功能移动代码。动态语言重构较为困难。
- 划分之后,可使用Structure 101等工具分析包之间的依赖。
- 依照代码划分,逐步将单块系统分离为多个微服务。
数据库:
数据库表同样需要划分到不同服务中,这一步往往是最棘手的。
可使用SchemaSpy等工具查看表之间的约束关系。
- 打破外键关联
使用接口访问其他服务的数据,而不是直接查询数据库。
移除分属不同服务的数据库表的外键关联,在代码中实现一致性检查。 - 共享静态数据
将多个服务共享的静态数据放入配置文件中。 - 共享数据
若多个服务都读写同一份数据,可以创建一个新的服务来管理这份数据,其他服务通过接口访问这个新服务。 - 共享表
若两个服务访问同一个数据库表的不同字段,可以拆分为两个表。
在实践中,可以先分离代码和数据库,稳定运行之后,再进行服务拆分。
事务边界:
单块系统可以用事务保证操作要么全部发生,要么全部不发生。
考虑这样一个微服务系统,用户下单时,用户服务创建订单,仓库服务创建发货记录,此时有可能创建订单成功,创建发货记录失败。有几种处理方式:
- 再试一次:将失败的操作记录下来,之后再次进行触发,以修复这个问题。这也叫做最终一致性。
- 拒绝整个操作,对于成功的操作,发起一个补偿事务来抵消,若补偿事务失败则需要重试。
- 分布式事务,使用事务管理器统一编配各个服务中的事务。常用的算法是两阶段提交:首先是投票阶段,每个参与者告诉事务管理器是否应该继续。如果事务管理器收到的所有投票都是成功,则会告知它们进行提交操作。只要收到一个否定的投票,事务管理器就会让所有的参与者回退。
- Saga分布式事务:与两阶段提交不同,Saga可以协调状态中的多个更改,同时避免长时间资源锁定。
具体算法为:将全局事务拆分为多个本地子事务,每个子事务由一个服务完成;正向执行:按顺序执行所有子事务;逆向补偿:若某个子事务失败,按反向顺序触发补偿操作。
Temporal等工具提供了Saga流程编排框架。
报表:
报表通常需要整合组织内各个部分的数据,获取数据有几种方式:
- 服务调用:直接通过API查询数据,适合简单的、数据量较小的报表。服务还可以提供特定的API,将数据导出到文件中。
- 数据导出:使用一个独立程序直接访问各个服务的数据库,把数据导出到单独的报表数据库中。
- 事件数据导出:编写独立的事件订阅器把数据导出到报表数据库中。
拆分方式——并行运行:
并行运行旧的单体服务和拆分后的微服务,并同时向两个系统都发送请求,并比较结果。
4 部署
CI(持续集成):保证新提交的代码与已有代码进行集成,从而让所有人保持同步。CI服务器会检测到代码已提交,然后验证代码是否通过编译以及测试能否通过。生成构建物的所有代码都位于版本控制中,构建物只生成一次,然后在所有环境中部署。
微服务持续集成的两种方式:
- 只有一个大的代码库,一次构建生成所有服务。适合项目初期一个团队维护所有代码。
- 每个微服务有自己的代码库,与相应的CI绑定。
持续交付:建立构建流水线,将构建分解为多个阶段,持续的反馈信息
关于平台特定的构建物,可选择的技术有:
- 运维管理工具如Chef、Puppet、Ansible等
- 操作系统支持的构建物,如CentOS的RPM包
- 定制化的虚拟机镜像,并可以进一步将服务包含到镜像中
服务配置:只创建一个构建物,将不同环境的配置单独管理
自动化:微服务需要管理多台主机,需要将主机控制、服务部署、监控、日志收集等工作自动化。
从开发测试到生产环境,配置文件和部署方式应当尽量相同,避免有些问题到生产环境才发现。
通过 Docker 和 Kubernetes,开发者可以构建一个可靠、可伸缩和易于管理的微服务架构。Kubernetes特别适合于管理大型、复杂的微服务部署,而Docker则简化了应用的打包和分发过程。
Docker 是一个开源的容器化平台,可将应用及其依赖打包成一个轻量级、可移植的容器,然后在任何支持Docker的环境中运行:
- 为每个微服务创建一个Docker镜像。
- 构建镜像,并存储到Docker仓库。
- 在目标主机上运行容器。容器可在单个或多个主机上运行,但手动管理这些容器的部署、扩展和网络连接可能会变得复杂。
Kubernetes(K8s) 是一个用于自动部署、扩展和管理容器化应用的开源系统。它提供了更高级的抽象和自动化,使得在大规模环境中管理微服务成为可能:
- 创建Kubernetes配置:为每个微服务编写Kubernetes配置文件,定义了如何部署和管理微服务。
- 搭建Kubernetes集群:可以是本地的(如Minikube),云提供商的(如Google Kubernetes Engine或Amazon EKS)。
- 部署微服务:将配置文件应用到Kubernetes集群中,Kubernetes将根据配置文件中的指令创建和管理容器。
- 服务发现与负载均衡:Kubernetes内置的服务发现和负载均衡能力可以自动地将请求分发到正确的微服务实例。
- 扩展和自愈:Kubernetes可以根据负载自动扩展微服务的副本数量,也能在微服务实例失败时自动重新部署实例,确保系统的可用性和弹性。
- 更新和回滚:Kubernetes支持声明式的更新,允许开发者通过更新配置文件来滚动更新微服务,同时也支持回滚到之前的版本。
Kubernetes 结合 Helm(包管理工具)和 Operator(自动化运维框架),可以实现更高效的声明式管理。
GitOps:
基础设施的期望状态在代码中定义并存储在源代码管理中。当这些期望状态发生变化时,某些工具(如Flux)会自动将更新后的期望状态应用于正在运行的系统中。这样可以为开发人员提供一个简化的工作流。
5 测试
《敏捷软件测试》中的测试象限:
由底层到上层:
- 单元测试:测试一个函数调用,面向技术而非业务,可在代码重构时快速发现问题
- 服务测试:绕开用户界面,只测试一个单独的服务,需要给外部合作者打桩
- 端到端测试:直接操作用户界面,覆盖整个系统
越底层的测试速度越快,反馈周期越短,越容易定位问题。要实现快速的持续集成,需要增加底层测试,减少上层测试。
服务测试:
- 打桩:为被测服务的请求创建一些有着预设响应的打桩服务
- mock:进一步验证请求本身是否被正确调用
端到端测试:任意服务的构建都触发端到端测试,重点测试少量核心场景。
使用契约测试来替换端到端测试:通过以隔离检查集成点上的每个应用的方式,确保应用发送或接收的消息符合调用双方共识。分为消费者驱动和提供者驱动两种模式。可使用Pack、SCC等工具。
Pack实现消费者驱动的契约测试:
部署后测试:
- 冒烟测试:快速验证基本功能,可手动或自动执行。
- 蓝/绿部署:部署两份软件,一份接受真正的请求,一份用于测试。前提是能够切换流量。
- 金丝雀发布:将部分生产流量引流到新部署的系统。可选择分流或者复制请求。
在平均故障间隔时间(MTBF)和平均修复时间(MTTR)之间权衡优化。
6 监控
监控小的服务,然后聚合起来看整体:
数据采集:
- Zabbix:监控CPU、内存等硬件信息以及网络流量
- OpenTelemetry(OTel):标准化遥测数据采集框架,提供SDK采集应用性能数据,跨服务传递追踪上下文(Trace ID、Span ID),实现分布式链路追踪。与 Kubernetes、 Istio深度集成。
数据存储与分析:
- Loki:专注于低成本、高效率的日志存储
- Prometheus:存储时序数据,设置监控告警,支持高效时间序列查询、以及标签灵活过滤和聚合
- Jaeger:分布式追踪系统,用于分析微服务调用链(如火焰图、依赖拓扑),便于定位慢请求、服务间延迟问题
- Grafana:可视化与分析平台,提供交互式仪表盘、实时数据图表,可设置告警
- EFK方案:Fluentd/Filebeat收集过滤日志,Elasticsearch提供存储和查询,Kibana提供数据可视化
7 安全
身份验证(Authentication)和授权(Authorization):主体通过身份验证,然后把主体映射到其可以进行的操作中。
传统单体应用登录 (结合Session和Cookie):
- 用户输入账号密码登录系统
- 服务端创建session(key-value格式)来保存登录状态
- 将session的key返回给浏览器,用cookie(字符串)进行存储
- 浏览器再次访问系统时,在请求中携带cookie信息
- 服务器查询session判断登录状态
单点登录(SSO):在整个分布式系统中,主体有单一的身份标识且只需进行一次验证
- 主体访问资源时,定向到身份提供者进行身份验证
- 身份提供者可以是内部服务,或外部托管系统(如google登录)
- 通过身份验证后返回token(一种共享令牌,包含用户信息)
- 浏览器请求服务提供者并携带token,并将token存到本地
- 服务提供者收到请求后,向身份提供者验证token,然后返回资源
- 主体访问分布式系统其他资源时,直接携带本地的token
常见的单点登录实现有CAS、SAML、OpenID Connect等。单点登录可以使用共享库,或者在网关统一处理。
授权:对用户分配某些角色,并将不同的权限赋予不同角色。建议按照组织的工作方式对角色建模,在各个服务内部进行细粒度的权限管理。
服务间的身份验证和授权,有几种方式:
- 边界内可信:在边界内对服务的任何调用都是默认可信的。有一定的安全风险。
- SSL或TLS证书。管理证书较为复杂,只在敏感数据使用。
- 使用OpenlD Connect。
- 使用HMAC对请求进行签名。可以检测出中间人篡改请求。AWS s3使用的方式。
- APl密钥,使用公钥私钥对,服务可以识别出是谁在进行调用。谷歌、AWS等服务商的公共API均使用这种方式。
混淆代理人问题:攻击者采用一些措施欺骗代理服务,让它调用其下游服务。比如请求其他人的私人信息。解决方式:每个服务都验证请求方身份。
加密:
- 使用广泛认可的加密算法,而不是自己发明
- 静态数据使用AES对称加密算法
- 密码使用加盐密码哈希技术,以防御彩虹表攻击
- 使用单独的密钥库管理密钥
- 只在需要时进行解密,不存储解密后的数据,也不将敏感数据写入日志
- 对于需要加密的数据,其备份也需要加密,且需要知道用哪个密钥来处理哪个版本的数据
防火墙:通过预定义的规则允许或拒绝数据包的传输,根据IP、端口、包头信息等进行限制
入侵检测/防御系统(IDS/IPS):在可信范围内进一步寻找可疑行为
网络隔离:把服务放在不同的网段中,以此定义互联规则
操作系统:给用户尽量少的权限;定期为软件打补丁;使用AppArmour等安全加固组件
OWASP TOP10:OWASP发布的十大最严重、最普遍的Web应用程序安全漏洞
序号 | 安全问题 | 描述 |
---|---|---|
1 | Broken Access Control (权限控制失效) | 未能正确实施限制用户访问的措施 |
2 | Cryptographic Failures (加密机制失效) | 加密措施的失败导致敏感信息被泄露 |
3 | Injection (注入式攻击) | 通过输入恶意数据来利用系统的安全漏洞 |
4 | Insecure Design (不安全设计) | 缺乏安全控制或者安全控制设计不当 |
5 | Security Misconfiguration (安全设定缺陷) | 安全设置被配置不当导致的安全问题 |
6 | Vulnerable and Outdated Components (危险或过旧的组件) | 使用已知存在漏洞的组件或未及时更新组件 |
7 | Identification and Authentication Failures (认证及验证机制失效) | 系统未能正确实现身份识别和认证机制 |
8 | Software and Data Integrity Failures (软件及资料完整性失效) | 未能确保软件和数据在传输或存储过程中的完整性 |
9 | Security Logging and Monitoring Failures (安全记录及监控失效) | 缺乏足够的日志记录和监控 |
10 | Server-Side Request Forgery (SSRF) (服务器端请求伪造) | 攻击者诱使服务器端应用发起请求以访问内部系统 |
渗透测试:模拟外部攻击,用以评估系统安全性。
8 康威定律和系统设计
康威定律:任何组织在设计一套系统时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。
组织的耦合度越低,其创建的系统的模块化就越好,耦合也越低。
团队内部沟通成本高,会导致重构困难,最终导致代码难以维护。
特性团队:一个小团队负责开发一系列特性需要的所有功能,即使这些功能需要跨越组件/服务边界。
服务根据业务领域,而不是技术进行建模,以避免出现特性团队。
内部开源:若团队间无法避免共享服务,可以允许其他成员提交代码,由核心团队进行审批。
软件系统也会反过来影响组织结构。
9 规模化微服务
规模化后小概率故障总是会发生:
- 级联故障:指在分布式系统中,由于单个节点故障引发的连锁反应。
- 功能降级:当功能不可用或负载过高时,直接停掉这些功能或降低其质量,以保证其他功能正常运行。
- 模拟故障:在生产环境主动引发故障,如关停服务、注入延迟等,确保系统的健壮性。
- 超时:给所有的跨进程调用设置超时时间,当超时发生后,记录到日志里看看发生了什么,并相应地调整它们。
- 断路器:当对下游资源的请求发生一定数量的失败后,断路器会打开,接下来请求会快速地失败。一段时间后,发送一些请求查看下游服务是否已经恢复,如果它得到了正常的响应,将重置断路器。
- 舱壁(Bulkhead):隔离每个工作负载或服务的关键资源,如连接池、内存和 CPU。使用舱壁避免了单个工作负载消耗掉所有资源,从而导致其他服务出现故障。
幂等:对幂等操作来说,其多次执行所产生的影响,均与一次执行的影响相同。
比如获取积分是幂等操作,增加积分不是幂等操作。但如果给增加积分操作关联一个订单号,一个订单号只能增加一次积分,就会变成幂等操作。
扩展:
- 垂直扩展:更换更强大的主机
- 拆分负载:多服务、多主机
- 分散风险:不同服务运行在不同的物理机、甚至不同的数据中心上
- 负载均衡:使服务更有弹性,避免单点故障,并对消费者透明
- 基于worker的系统:和负载均衡类似,适合处理批量或异步作业,如Hadoop
- 重新设计:你的设计应该考虑10倍容量的增长,但超过100倍容量时就要重写了(by Jeff Dean)
扩展数据库:
- 数据持久性:备份数据
- 扩展读操作:创建只读副本,此时读到的数据可能是失效的,但最终能读到一致的数据 (称作最终一致性)
- 扩展写操作:分片存储数据,对数据的关键字进行哈希,以此决定存到哪个分片;会增加查询复杂性
- 读写分离:写数据可以同步或异步进行,读到的数据可能是过期的
缓存
- 存储之前操作的结果,以便后续请求可以使用,而不需要重新获取资源或计算
- 分为客户端缓存、代理缓存(反向代理或CDN)、服务器缓存(Redis、内存缓存)
- HTTP缓存技术:cache-control、Expires、Etag
- 写缓存:先写入缓存,并在之后某个时刻写入数据库
- 缓存可以在出现故障时实现弹性,下游服务不可用时仍可读/写缓存
- 隐藏源服务:一种缓存实现方式,缓存不可用时不立即请求源服务,而是异步重建缓存,避免源服务收到过量的请求
自动伸缩:
- 响应型伸缩:负载增加或某个实例发生故障时,来增加额外的实例,或在不需要时移除它们
- 预测型伸缩:特定时间段预期负载升高,主动进行扩容
CAP定理:
在分布式系统中有三方面需要彼此权衡:一致性(consistency)、可用性(availability)和分区容忍性(partition tolerance)。我们最多只能保证三个中的两个。
一致性:当访问多个节点时能得到同样的值。
可用性:每个请求都能获得响应。
分区容忍性:集群中的某些节点在无法联系后,集群整体还能继续进行服务。
案例:两个主数据库,通过彼此通信进行数据同步,若两个数据库的网络连接断开
- 牺牲一致性:完全不停用服务。如果更改了DC1的数据,DC2的数据库将看不到它,这意味着DC2上的请求看到的是失效的数据。换句话说,我们的系统仍然可用,两个节点在系统分区之后仍然能够服务请求,但失去了一致性。这通常被称为AP系统。网络恢复后数据重新同步,这种做法称为最终一致性。
- 牺牲可用性:每次处理请求时都需要知道数据是否一致,如果网络连接断开,只能拒绝请求。换句话说,我们牺牲了可用性,系统是一致的和分区容忍的,即CP系统。实现一致性非常困难,需要为读取操作添加分布式事务。
服务发现: (如何找到服务所在的地址)
- DNS:将域名关联到服务所在的主机或负载均衡器上。
处理不同环境中的服务实例可以使用模板域名,比如形如 <服务名>-<环境>.musiccorp.com 的模板,也可以在不同的环境中使用不同的域名服务器。 - Consul:支持配置管理,以及动态服务注册与发现。
- Eureka:Netflix的开源系统,提供了服务发现以及负载均衡功能。
服务网格和API网关:
- API网关位于系统的边界,管理外部世界对内部微服务的访问
- 服务网格是一个基础设施层,用于处理服务间通信,支持服务发现、故障处理、服务治理、通信加密等微服务基础能力,与业务逻辑彻底解耦
- 目前主流技术是:Istio、Linkerd 等服务网格技术通过 Sidecar 模式统一管理服务间通信
如上图,应用服务之间通过 SideCar 进行通信,整个服务通信形成图中的蓝色网络连线,所有蓝色部分就形成了 Service Mesh。
文档服务: (微服务API的文档)
- Swagger:服务提供与其格式相匹配的API描述,以此产生Web文档。
- HAL:超文本应用程序语言,如果你已经在使用超媒体控制,那么可以很容易的提供一个HAL浏览器,否则不推荐使用HAL。