什么是微服务
微服务是一种典型的“高内聚、低耦合”的软件架构,每个微服务实现了各种业务功能,微服务架构天然的支持持续集成和持续部署。
简单来说,微服务架构就是将一个完整的应用从数据存储开始垂直拆分成多个不同的服务,每个服务能独立部署、独立维护、独立扩展。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务。通常,每个任务代表着一个小的业务能力。
为什么要用微服务
我们先来看看传统的单体式应用架构,比如电商系统,有用户登录注册业务,订单业务、支付业务、退款业务、评论业务等。当这些业务都糅杂成一个应用来实现时,这种方式有很大的局限性,我们来看看这种单体式架构有哪些问题。
- 复杂性逐渐变高
项目有几十万行代码,各个模块之间区别比较模糊,逻辑比较混乱,代码越多复杂性越高,越难解决遇到的问题。 - 技术债务逐渐上升
人员流动问题,可维护性变差。 - 部署速度逐渐变慢
代码越多编译越慢,部署越慢,变更一部分配置,影响范围太大。 - 阻碍技术创新
想要改变些什么,但历史包袱太重。 - 无法按需伸缩
比如cpu密集型的模块、大内存模块等。
一旦应用程序成了一个庞大、复杂的单体,开发会陷入一个痛苦的境地,敏捷开发和交付的任何一次尝试都将原地徘徊。主要问题是应用程序实在非常复杂,其对于任何一个开发人员来说显得过于庞大。最终,正确修复 bug 和实现新功能变得非常困难而耗时。
为了解决这个问题,就诞生了一个新的概念 ——”微服务”
微服务如何解决复杂问题
- 每个服务专注实现不同的特性或功能
例如订单管理、客户管理等。每一个微服务都是一个迷你应用,它自己的六边形架构包括了业务逻辑以及多个适配器。 - 每个微服务会暴露一个供其他微服务或客户端调用的 API
其他微服务可能实现了一个 web UI。在运行时,需要服务间相互访问。
一个单体应用如何拆分成微服务
一般按照“功能模块”来划分,比如用户管理模块、支付模块、订单模块等,也可以根据康威定律按照团队组织来拆分,当然最主要是根据实际业务场景去划分。
微服务架构特点
微服务架构模式可以实现每个微服务独立部署,独立扩展。微服务的架构优势包括但不限于:
- 独立扩展
基于Kubernetes平台的Pod级别扩展 - 快速发布
功能简单,测试简单,无过多依赖,配置简单,发布简单。 - 快速试错
快速试错-小步快跑 - 快速应用新技术
快速开发,快速上线,快速调整,易于开发和维护,启动较快,局部修改容易部署,技术栈不受限,按需伸缩
微服务的劣势
- 性能损失,微服务之间都是通过restfull或grpc的方式进行通信。
- 系统复杂,一个应用系统会有非常多的服务。
- 服务太多监控复杂,排查定位问题较麻烦。
- 微服务架构不适用于资源密集型应用,如对单机性能要求高的数据库、存储系统等。
- 对运维要求比较高。
- 接口调整成本高。
使用微服务面临的一些问题
- 如何发现服务
- 如何对服务链路进行追踪
- 如何管理配置
- 如何对服务API进行控制
- 如何持续集成和持续部署
- 微服务如何拆分
微服务有哪些特性
分布式链路跟踪
当微服务被引入后,每个服务的运行时长会变得很难追踪,当调试一个问题时,因为微服务很多(达到成百上千时)此时去找问题就会很麻烦,那么通过Tracing的方式就可以很容易的找到调用的东西从而大大降低了寻找问题的时间成本。主流的基于Service Mesh的微服务技术架构,可以使用Jaeger来做服务的请求等链路追踪。
熔断
当一个服务因为各种原因停止响应时,调用方通常会等待一段时间,然后超时或者收到错误返回。如果调用链路比较长,可能会导致请求堆积,整条链路占用大量资源一直在等待下游响应。所以当多次访问一个服务失败时,应熔断,标记该服务已停止工作,直接返回错误。直至该服务恢复正常后再重新建立连接。
降级
什么是服务降级?当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作。
当我们去秒杀或者抢购一些限购商品时,此时可能会因为访问量太大而导致系统崩溃,此时开发者会使用限流来进行限制访问量,当达到限流阀值,后续请求会被降级;降级后的处理方案可以是:排队页面(将用户导流到排队页面等一会重试)、无货(直接告知用户没货了)、错误页(如活动太火爆了,稍后重试)。
降级和熔断区别
- 触发原因不太一样,服务熔断一般是某个服务故障引起,而服务降级一般是从整体负载考虑;
- 管理层次不太一样,熔断是一个框架级的处理,每个微服务都需要(无层级之分),而降级一般对业务有层级之分(比如降级一般是从最外围服务开始);
- 实现方式不太一样;
限流
一个服务挂掉后,调用者或者用户一般会习惯性地重试访问。这导致一旦服务恢复正常,很可能因为瞬间网络流量过大又立刻挂掉,因此服务需要能够自我保护 —— 限流。限流策略有很多,最简单的比如当单位时间内请求数过多时,丢弃多余的请求。典型的如每年双11时,部分用户抢单时提示失败提示“网络问题,请稍后重试”。
服务发现
服务调用者使用Service名称去访问其他服务,通过K8s Coredns和Etcd组件反向代理到服务的IP地址+端口。
健康检测
比如给服务增加延时,给服务注入错误等。
负载均衡和流量网关&流量调度
在K8s+Istio的方案中,在集群内部使用K8s service或virtual service做四层负载均衡,在集群外部使用ingress或gateway做流量网关和流量调度,在集群内部使用Envoy Sidecar做流量控制等等。
灰度发布和A/B测试
把请求路由到服务的指定版本;根据服务版本权重拆分流量;根据请求信息路由到服务的不同版本。
鉴权和访问控制
鉴权和访问控制放在API gateway 这一层来做,比如客户端调用登录接口,passport 会把 token 和 userid,传到 API gateway,API gateway 再把相应的 token 传到这个 APP 端。客户端下次请求就拿 token 请求,如果 token 验证不过,就返回客户端。如果验证通过再调用后端不同的服务获取结果,最后返回结果给客户端。
自动弹性伸缩
基于资源使用率的云服务器和K8s Pod的自动弹性伸缩。
Go微服务架构技术
当前的主流趋势是使用Go技术栈来开发微服务,那么针对上述这些问题和特性有哪些开源的解决方案呢?用到的技术栈,包括但不限于:
- 开发语言及其框架
Golang、gRPC、go-micro、protobuf、Gin、Gorm - 中间件
Kafka、Redis、MySQL、MongoDB、Consul/Etcd - 容器化与平台
Docker、Kubernetes - 消息系统与持续集成
Jenkins、IM
GRPC介绍
JSON 或 XML 协议 API
微服务之间可使用基于 HTTP 的 JSON 或 XML 协议进行通信:服务 A 与服务 B 进行通信前,A 必须把要传递的数据 encode 成 JSON / XML 格式,再以字符串的形式传递给 B,B 接收到数据需要 decode 后才能在代码中使用,优缺点如下:
- 优点:数据易读,使用便捷,是与浏览器交互必选的协议
- 缺点:在数据量大的情况下 encode、decode 的开销随之变大,多余的字段信息导致传输成本更高
这种协议的API在 Browser / Server 架构中用得很多,以方便浏览器解析。但在微服务之间通信时,若彼此约定好传输数据的格式,可直接使用二进制数据流进行通信,不再需要笨重冗余的元数据。
gRPC 简介
gRPC 是谷歌开源的轻量级 RPC 通信框架,gRPC 使用HTTP 2.0 协议并用Protobuf作为序列化工具,使用二进制帧进行数据传输,还可以为通信双方建立持续的双向数据流,使得 gRPC 具有优异的性能。与REST不同的是,REST是基于HTTP1.1 JOSN格式的一种轻量级服务模式,那么从这两方面来对比gRPC与REST就比较容易了。
protobuf是一款平台无关,语言无关,可扩展的序列化结构数据格式。所以很适合用做数据存储和作为不同应用,不同语言之间相互通信的数据交换格式,只要实现相同的协议格式即同一 proto文件被编译成不同的语言版本,加入到各自的工程中去。这样不同语言就可以解析其他语言通过 protobuf序列化的数据。
常见的微服务架构最前面是load balance,后面紧接着是API gateway,主要做一些聚合的工作。对外采用REST API方式相互调用,对内则采用gGPC API的方式相互进行调用。
Go-Micro介绍
Go-Micro是go语言下的一个很好的rpc微服务框架,功能很完善,主要特性有:
- 服务间传输格式为protobuf,效率高,性能好。
- go-micro的服务注册和发现是多种多样的。
- 主要的功能都有相应的接口,只要实现相应的接口,就可以根据自己的需要订制插件。
Go-micro框架组成的包
- transport 用于同步消息
Transport是服务与服务之间同步请求/响应的通信接口。和Golang的net包类似,但是提供更高级的抽象,请允许我们可以切换通信机制,比如http、rabbitmq、websockets、NATs。传输也支持双向流,这一强大的功能使得客户端可以向服务端推送数据。
- broker 用于异步消息
Broker提供异步通信的消息发布/订阅接口。对于微服务系统及事件驱动型的架构来说,发布/订阅是基础。一开始,默认我们使用收件箱方式的点到点HTTP系统来最小化依赖的数量。但是,在go-plugins是提供有消息代理实现的,比如RabbitMQ、NATS、NSQ、Google Cloud Pub Sub等等。
- Codec 用于消息编码
Codec编码包用于在消息传输到两端时进行编码与解码,可以是json、protobuf、bson、msgpack等等。与其它编码方式不同,我们支持RPC格式。所以我们有JSON-RPC、PROTO-RPC、BSON-RPC等格式。
编码包把客户端与服务端的编码隔离开来,并提供强大的方法来集成其它系统,比如gRPC、Vanadium等等。
- registry 用于服务发现
Registry注册提供了服务发现机制来解析服务名到地址上。它可以使用Consul、etcd、zookeeper、dns、gossip等等提供支持。服务使用启动注册关机卸载的方式注册。服务可以选择性提供过期TTL和定时重注册来保证服务在线,以及在服务不在线时把它清理掉。
- selector 用于负载均衡
Selector选择器是构建在注册这上的负载均衡抽象。它允许服务被过滤函数过滤掉不提供服务,也可以通过选择适当的算法来被选中提供服务,算法可以是随机、轮询(客户端均衡)、最少链接(leastconn)等等。选择器通过客户端创建语法时发生作用。客户端会使用选择器而不是注册表,因为它提供内置的负载均衡机制。
- client 用于发送请求
Client客户端提供接口来创建向服务端的请求。与服务端类似,它构建在其它包之上,它提供独立的接口,通过注册中心来基于名称发现服务,基于选择器(selector)来负载均衡,使用transport、broker处理同步、异步消息。
- server 用于处理请求
Server服务端包是使用编写服务的构建包,可以命名服务,注册请求处理器,增加中间件等等。服务构建在以上说的包之上,提供独立的接口来服务请求。现在服务的构建是RPC系统,在未来可能还会有其它的实现。服务端允许定义多个不同的编码来服务不同的编码消息。
服务发现
在服务发现框架中,一般使用Consul或Etcd用于实现分布式系统的服务发现与配置。Consul是分布式的、高可用的、可横向扩展的。我们看下面的一幅图片来理解:
在K8s环境中,服务A-N把当前自己的网络位置注册到K8s集群的Etcd服务中,服务发现就以K-V的方式记录下,K一般是服务名,V就是IP:PORT,如AppName.NameSpace.svc.cluster.local:8000。客户端在调用服务A-N的时候,就跑去服务发现模块问下它们的网络位置,然后再调用它们的服务。最后客户端依赖的配置文件使用Consul管理,这样客户端完全不需要记录服务A-N的网络位置,这个过程大体是这样,里面包含的东西还很多,这样表示只是方便理解。
OK,关于Go微服务架构技术之概念篇就写到这里,后面尝试通过写Demo程序来更深入的理解。