一个架构的设定关乎于相应软件未来的生命周期,架构是一个很宏大的命题,每个程序员的架构思维都是一点点积累的。没有最完美的架构,只有最合适的架构。所谓“架构即决策”,是在一个有约束的盒子中寻求最优解。这个有约束的盒子是团队经验、成本、资源、进度、业务所处阶段等编织、掺杂在一起的综合体。本质上无优劣,但是存在恰当的架构用在合适的软件系统中,而这些就是决策的结果。
一张完整架构图谱:
因为单机架构单一又简单,没有什么研究的必要,于是只对微服务架构进行深入。
微服务是一种软件架构风格,它是以专注于单一责任与功能的小型功能区块为基础,利用模块化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关的 API 集相互通信。
微服务架构有别于更为传统的单体服务,可将应用拆分成多个核心功能。每个功能都被称为一项服务,可以单独构建和部署。
这也体现了可扩展的基本思想:将原本大一统的系统拆成多个小部分,扩展时只修改其中一部分,通过这种方式减少改动范围,降低改动风险。
微服务架构涵盖了服务的多个方面,包括网关、通信协议、服务注册/发现、可观察性、如何合理的划分等等。
在做微服务之前我们首先要想明白我们现有系统面临什么样的问题,为什么需要微服务,随后才是怎么做。
微服务很多核心理念其实在半个世纪前的一篇文章中就被阐述过了,而且这篇文章中的很多论点在软件开发飞速发展的这半个世纪中竟然一再被验证,这就是康威定律,有兴趣可以去看下这篇相关的文章:链接
在康威的这篇文章中,最有名的一句话就是:
Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations.
中文直译大概的意思就是:设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。
最初这篇文章只是描述作者自己的发现和总结,后来“人月神话”中,引用这个观点,并将其“吹捧”成现在熟知的“高位定律”。
其中的一些核心观点可以概括如下:
但是当我们的业务和组织架构复杂度比较高的时候,很多概念只从技术角度很难去抽象,这就需要我们自上而下,建立起通用语言,让业务人员和研发人员说一样的话,把思考层次从代码细节拉到业务层面。越高层的抽象越稳定,越细节的东西越容易变化。
通过对不同领域的建模,逐步确定领域范围和业务边界,这也就是领域驱动设计(DDD)。
DDD 是一种在面向高度复杂的软件系统时,关于如何去建模的方法论,它的关键点是根据系统的复杂程度建立合适的模型,DDD 中的界限上下文也完美匹配了微服务的高内聚、低耦合特性,这也为我们微服务的划分提供了强有力的基础。
DDD 实施的一般步骤是:
但是 DDD 也不是银弹,特别是在一些新业务场景,本身就充满了很多的不确定性,一次性把边界划清楚并不是一件很容易的事。
大家在一个进程里,调整起来会相对容易,然后让不同的界限上下文各自演化,等到了一定程度之后再考虑微服务也是一个不错的选择。
作为微服务的统一入口,也肩负着整个微服务的流量接入、管理、聚合、安全等,从服务分层的角度可以划分为接入网关和业务网关。
接入网关提供最基础的流量接入和安全防护能力,侧重于全局,与业务无关。
**域名&DNS:**作为服务的流量入口,对外通过域名和 DNS 提供服务,国内域名厂商一般都依托于共有云或被共有云厂商收购,用来完善自由的云生态。
像阿里的万网,腾讯的 DNSPod 等,也有国外的 AWS,GoDaddy 和 Namecheap 等,可以用作 .me 等国内无法托管或备案域名的管理。
其次也可以借助DNS(HTTPDNS、EDNS)实现跨地域、运营商网络等负载均衡,实现异地多活、就近访问、容灾等。
**负载均衡(LB):**主要负责请求的转发代理,按机器负载来分配流量等,对外提供 VIP,这里的负载可以宽泛的理解为系统的压力,可以用 CPU 负载来衡量,也可以用连接数、I/O 使用率、网卡吞吐量等来衡量。
负载均衡器按服务层级来划分,除了前边提到的 DNS,还有集群级别的硬件负载均衡,以及机器级别的软件负载均衡。
DNS/硬件负载均衡(F5/A10)主要用来应对海量用户的访问,中小量用户使用无疑会增加更多的维护和采购成本。
软件负载均衡可以选择自研或上云,LVS、Keepalived 主要用于四层(IP+端口)的负载均衡,在四层的基础之上如果要实现应用层(域名/URL/用户会话)等的 7 层负载均衡,可以使用 Nginx、Keepalived 的组合。
除此之外,网关也负责服务整体的安全防护,SSL,IPV6 等:
业务网关作为业务的最上层出口,一般承担起业务接入或者 BFF 的工作,例如基础的路由、鉴权、限流、熔断降级、服务聚合、插件化能力,并可以通过可视化界面管理网关配置。
可选框架有基于 OpenResty 的 Kong、APISIX 以及其他语言相关的 SpringCloud Gateway、gRPC-Gateway 等等。
国内开源的 Goku、Kratos、go-zero go 框架,有很多比较有意思的组件实现,我们日常业务上也可以借鉴。
**鉴权:**鉴权的目的是为了验证用户、请求等的有效性,例如用户身份鉴权(JWT/Oauth2/Cookie),请求鉴权(请求签名、请求加密),鉴权逻辑也花样繁多,大多需要基于业务定制化,通过网关插件能很好的集成进来。
**限流:**限流是为了做一定的流量控制,防止对系统产生过大压力从而影响整个服务。可以基于单台机器或整个集群限流,常见的方式有限制总量和限制速率,超过的则排队或丢弃,例如令牌桶(弹性)/漏桶(匀速)算法。
**熔断降级:**熔断作为服务断路器,当下游的服务因为某种原因突然变得不可用或响应过慢(这里既可以指单次请求也可以指一段时间),上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,直接返回,这样也能对整体链路起到保护作用。
如果目标服务情况好转则恢复调用,同时结合降级策略提升服务的鲁棒性。常见的有Hystrix/Resilience4J(Hystrix 虽然已停止更新,但现有功能已经能满足大多业务场景)。
**重试:**大量网络 IO,避免不了会出现因网络抖动,出现连接失败或者超时,重试可以提高请求的最终成功率,削平服务毛刺。
但重试也有可能放大故障,所以可以结合退避策略(backoff)、限制单点重试、限制链路重试这些策略进行优雅的重试,同时也可以采用更加激进的“对冲请求”提前(tp99 时间未响应时)发起重试请求,降低系统时延。
**插件化:**各个网关集成插件的方式尽不相同,但是目的都是为了集成技术人员编写的一些业务相关的通用能力,例如前边提到的身份鉴权、请求鉴权等等。
另外作为业务网关插件,也可以编写一些基础业务(API 鉴权、请求格式化)逻辑,直接透传请求到服务层,省去很多 BFF 和上下游对接的工作。
**BFF:**Backend For Frontend,可以按照业务逻辑,以串行、并行和分支等结构编排多个服务 API,为服务提供聚合、适配、裁剪(只返回需要的字段)功能,核心是 API 的动态编排以满足日益增长的业务逻辑,降低前端与微服务之间的对接成本。
BFF 并不意味着只能由后端实现,也可以在前端通过 GraphQL 等 API 查询语言实现。
服务间的通信方式是在采用微服务架构时需要做出一个最基本的决策,统一的协议标准也能大大降低服务的联调和维护成本。
**HTTP REST:**REST 更确切的讲是指的 API 设计风格,而不是协议标准。通常基于使用 HTTP,URL,和 JSON 这些现有的广泛流行的协议和标准。符合 REST 设计风格的 API 称作 RESTful API。
在实际应用中大多实现的是伪 REST API,例如用 POST 请求同时实现资源的增删改,或者为了请求的扩展性,资源的增删改查都使用 POST JSON。
**RPC:**RPC 协议描绘了客户端与服务端之间的点对点调用流程,包括 stub、通信、RPC 消息协议部分。可以基于 TCP,也可以基于 http。
在实际应用中,还需要考虑服务的高可用、负载均衡等问题,所以产品级的 RPC 框架除了点对点的 RPC 协议的具体实现外,还应包括服务的发现与注册、提供服务的多台 Server 的负载均衡、服务的高可用等更多的功能。
目前的 RPC 框架大致有两种不同的侧重方向,一种偏重于服务治理(Dubbo、Motan),另一种偏重于跨语言调用(Thrift/GRPC)。
RPC vs HTTP REST 优点:
在一些特定场景,例如:OpenAPI、BFF 等,HTTP REST 可以更大程度上降低外部团队的接入成本。并且 RPC 也有调试不便、多语言互通需要对应的 SDK 支持这些问题,各有利弊。
综合考虑来看,除了一些特定场景,如果我们已经有相对完善的基础设施支撑(RPC 框架、服务治理),RPC 可以为一个更合适的选择。
服务注册主要是通过将微服务的后端机器 IP、端口、地域等信息注册起来,并结合一定的发现机制使客户端的请求能够直连具体的后端机器。
从实现方式上可以分为服务端模式与客户端模式:
**服务端模式:**也可以说是传统模式,通过借助负载均衡器和 DNS 实现,负载均衡器负责健康检查、负载均衡策略,DNS 负责实现访问域名到负载均衡器 IP/VIP 的映射。通过直接暴露域名和端口的方式提供客户端访问。
**客户端模式:**可以借助注册中心实现,注册中心负责服务的注册与健康检查,客户端通过监听配置变更的方式及时把配置中心维护的配置同步到本地,通过客户端负载均衡策略直接向后端机器发起请求。
从两种模式的实现方式上可以看出:
1、服务端模式注册与发现都由服务端完成,这样可以使客户端专注在自身的业务实现,但是由于依赖负载均衡器,也就是集中式的 proxy,proxy 需要维护双向连接,也很容易使自己成为系统瓶颈,可用性的高低直接决定了服务质量。
并且 DNS 缓存机制也会导致故障发生时,迁移并不能及时完成。当然在服务量少,且负载均衡器有 VIP 的情况下,我们也可以不使用 DNS。
2、客户端模式注册与发现由配置中心和客户端共同完成,通过分布式的方式,可以避免出现 proxy 节点性能瓶颈问题,但是可靠性与性能瓶颈很容器出现在配置中心上,并且客户端的也需要一定的接入成本。
好在开源的已经有很成熟的架构方案与丰富的客户端 SDK,例如 etcd/ZooKeeper/Consul。
Consul 提供开箱即用的功能,etcd 社区和接入易用性方面更优一些,他们之间的一些具体区别:
配置中心从使用场景来讲,一类是前边讲到的服务注册、发现和 KV 存储,例如 etcd/ZooKeeper/Consul,在 Kubernetes 场景下也可以通过 ConfigMap/Secret 将配置写入本地文件、环境变量或者共享的 Volume 中。
这样没有了中心服务的依赖和客户端的接入,可以实现一些老旧服务的无侵入式改造。
但是作为配置中心,除了基础的配置数据,一些情况下还要开放给非开发人员(测试、运维、产品)使用,完善的控制台、权限管理、Dashbord 的支持,也非常重要,这类可以参考 Nacos(阿里开源)/Apollo(携程开源)。
Nacos 在读写性能上优于 Apollo,但是功能特性(例如权限管理)稍逊于 Apollo。
在控制论中,可观察性是用系统输出到外部的信息来推断系统内部运运行状态的一种度量方式。
在云原生时代,容器和服务的生命周期是紧密联系在一起的,相较在传统的单体服务运行在物理主机或者虚拟机当中,排查问题的时候显得非常不便,这种复杂性导致了一个定义研发运营效率的 MTTR(平均故障修复时间)指标急剧增加。
所以这里更强调的是微服务的可观察性,需要提前想好我们要如何观察容器内的服务以及服务之间的拓扑信息、各式指标的搜集等,这些监测能力相当重要。
可观察性三大支柱围绕 Tracing(链路追踪)、Logging(日志)和 Metrics(度量)展开,这三个维度几乎涵盖了应用程序的各种表征行为,开发人员通过收集并查看这三个维度的数据时刻掌握应用程序的运行情况。
很长一段时间,这三者是独立存在的,随着时间的推移,这三者已经相互关联,相辅相成。
链路追踪为分布式应用的开发者提供了完整的调用链路还原、调用请求量统计、链路拓扑、应用依赖分析等工具,可以帮助开发者快速分析和诊断分布式应用架构下的性能瓶颈,提高微服务时代下的开发诊断效率以及系统的可观察性。
为了解决不同的分布式系统 API 不兼容的问题,诞生了 OpenTracing 规范,OpenTracing 中的 Trace 可以被认为是由多个 Spacn 组成的 DAG 图。
[Span A] ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C 是 Span A 的孩子节点, ChildOf)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G 在 Span F 后被调用, FollowsFrom)
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]
OpenTracing 专注在 tracing,除此之外还有包含了 Metrics 的 OpenCensus 标准,以及由 CNCF 推出,融合 OpenTracing 和 OpenCensus 的 OpenTelemetry。
OpenTelemetry 旨在实现云原生时代可观察性指标(Tracing、Logging、Metrics)的统一收集和处理,同时提供推动这些标准实施的组件和工具。
OpenTracing 中的佼佼者当属 Jaeger、Zipkin、Skywalking。他们之间的一些对比:
Zipkin 开源时间长,社区相对丰富,Jaeger 更加轻量,也是 Istio 推荐方案,SkyWalking 支持部分语言(Java、PHP、Python 等)的无侵入式接入。另外 APM(应用性能)监控的支持也会影响到我们的选型。
除此之外,面对线上海量请求,如果采用抽样采样策略,那就需要支持一定的流量染色,把我们核心关注的请求(例如链路中发生了错误、部分请求耗时过高等)都进行采样。
可以通过结合 opentelemetry-collector 以及开箱即用的 tailsamplingprocessor 构建 Pipeline 插件实现。
服务间的链路日志能否帮助我们判断错误发生的具体位置,这类业务日志主要集中在访问日志/打点日志等等。
随着大数据的兴起,我们对数据的分析解读能力越来越强,日志作为原始数据则体现出了更大的价值,例如用户的行为分析,反垃圾,舆情分析等等。
业务日志:这类日志重点在于通过不同级别的日志,及时发现分析系统存在的异常。
RFC 5424 定义的 8 中日志级别:
在实际使用过程中可能会对日志级别进行简化和调整,一般来讲 Warning 及以上的日志是需要重点关注的,需要做好及时的监控告警,Warning 以下的日志也可以辅助问题的定位。
日志写入可以选择写入消息队列,也可以选择落地磁盘,将关心的结构化或非结构化日志、业务模块信息(如果是细粒度的微服务,可以选择将日志放同一模块收集),以及级别、时间(who、when、where、how、what)等要素正确的写入正确写入后再收集到日志服务。
写入消息队列需要考虑消息队列的选型以及做好可用性和积压监控,写入磁盘需要考虑写入性能以及日志的切割清理,例如 Golang 的 zap+rotatelogs 组合。
日志收集的话,由于 Logstash 资源消耗相对比较大,虚拟机环境中可以使用 Filebeat 来替代,更严苛的线上或容器环境,可以使用 Fluentd/Fluentd Bit。日志最终汇总到 ES 和 Kibana 做展示,通过 Esalert 定制告警策略。
大数据日志:大数据日志本质上也对应着我们一定的业务场景,但大多是海量日志、高吞吐量场景,所以对海量日志的收集和存储是较大的挑战。
实现方案我们可以采用高吞吐量的流式中间件,例如 Kafka/Plusar 等,在结合流式处理(Flink)或者批处理(Spark)系统,将数据汇总到 Hadoop 进行分析,这里涉及到的中间件和数据库可参考后续章节。
指标是有关系统的离散的数据点,这些指标通常表示为计数或度量,并且通常在一段时间内进行汇总或计算。
一般用来做基础的资源监控和业务监控:
Zabbix 作为老牌的监控系统,适合更复杂的物理机、虚拟机、数据库等更复杂的场景,同时也拥有更丰富的图形化界面。
但是 Prometheus 作为云原生的代表作,与 Kubernetes、容器等能更好的结合,协同 Grafana 实现可定制化的界面,另外存储基于 TSDB,相比于关系型数据库也有更好的扩展性。
以 Prometheus 为例,支持的数据类型有:
Prometheus 指标支持 pull 和 push 模式:
我们前边讲的服务发现、熔断降级、安全、流量控制、可观察性等能力。这些通用能力在 Service Mesh 出现之前,由 Lib/Framework 通过一些切面的方式完成,这样就可以在开发层面上很容易地集成到我们的应用服务中。
但是并没有办法实现跨语言编程,有什么改动后,也需要重新编译重新发布服务。理论上应该有一个专门的层来干这事,于是出现了 Sidecar。
第一代 Service Mesh,像 Linkerd,后边又出现了第二代 Service Mesh,Istio,职责分明,分离出处数据面和控制面。
但是 Sidecar 作为代理层,避免不了性能损耗(CPU 序列化反序列化 UDS),所以 proxyless service mesh 重新被提起,和之前的 「RPC + 服务发现治理」区别是啥?
感觉这个名词营销味道略重。其实不能简单的 “Proxyless Service Mesh” 理解为 “一个简单的 RPC 框架,暴露了几个超时参数到配置中心来控制”,它重在统一协议、API。
这样就便于基于统一的协议实现 proxyless mesh 和 proxy mesh 的互通,可以同时满足性能敏感型和快速迭代型的业务场景。
他们相辅相成,丰富了 service mesh 的形态:
Service Mesh对于微服务基础设施的一种演进,但不代表他已经非常成熟了,相反像迁移成本高,甚至一些可用性设计还不如业务自己做那么灵活。
这些现实的问题还摆在面前,我觉得这也是属于技术进化的一种趋势,当一项技术足够成熟的时候,又回衍生出新的复杂度问题,从而又需要发展出新技术解决。