前段时间重读赵成老师《进化:运维技术变革与实践探索》一书时,脑海里就有个强烈的念头 -- 要系统性研究下微服务架构。从 IT 视角来看,系统架构决定了组织架构,也进化出了与之匹配的开发模式(如敏捷、DevOps)、运维模式(如停机变更、持续交付)。
网络安全漏洞更是随着架构和工具的实现,在不断变化,以Java技术栈为例,有框架漏洞(struts、spring、weblogic)、协议漏洞(RMI、反序列化),还有web容器漏洞(tomcat、jboss、Glassfish),如果没有架构知识做支撑,很难厘清各种名词之间的关系,从而只见树木不见森林,迷失在新技术名词之中。
于是花了些时间,对微服务知识做了梳理,并在组内进行了技术分享,遂有此文。本文偏向导论性质,旨在搭建框架,后续会有系列运维文章对提到的各个概念进行深入探讨,敬请期待。
全文分为四个部分:
文章较长,赶紧开始吧。
改编自《一文详解微服务架构》[1]
最近元宇宙很火,住在我隔壁的大葱决定创业,开发一款 NFT 藏品商城。创业初期哪哪都要花钱,本着能省则省的原则,大葱在腾讯云买了一台入门款 CVM,整个商城采用单体架构,部署在一台服务器上,并做了简单的前后台分离。
功能设计方面比较简单,只有三个核心模块:
网站已经 Ready,大葱作为B站坐拥200万粉丝的UP主,联合另一顶流谭Sir,发行了谭谈交通人物NFT,粉丝们蜂拥而入,业务火爆到不行。很快,就有其他竞争对手入局。为了赢得市场,大葱决定开展一些营销动作:
1、促销:618 满300减100,双11买一送一
2、增加渠道:全端覆盖,网站+APP+小程序
3、精准营销:针对用户进行分析,提供个性化服务
这些功能应该放在哪里实现呢?时间紧迫,那就先把1和3放在后端,2放在前端吧。大葱没写过移动APP和小程序,于是招了两个人,小草负责android、iOS,小蒜负责小程序、H5。
此时,架构方面存在不少问题,大致可以分为三类:
一、质量问题
1、数据库可用性:只要数据库出问题,所有服务中断。
2、数据库性能:管理后端加入了营销分析,每次对用户数据分析时,数据库性能都急剧下降。
二、效率问题
1、开发效率:三端之间存在业务逻辑重复的代码;数据有时通过DB共享,有时通过接口调用,调用方式混乱;单个应用为了对外提供接口,逻辑越来越复杂,包含了很多不属于它的逻辑。应用边界模糊,功能归属混乱。
2、维护效率:任何一个应用出问题,都会影响其他应用;即便很小的功能改动,也要整个应用一起发布;每次发布都要重启,导致业务中断,因此发布经常安排在凌晨1点;发布后为了验证应用运行正常,还要有人持续值守到第二天白天用户高峰期。
三、其他问题
公用功能到底该建设在哪个应用上,各团队争论很久,经常推诿扯皮。最后要么各干各的,或者随便这个地方实现,但是无人维护。
在这种架构中,每个人都只关注在自己的一亩三分地,缺乏全局的、长远的设计。长此以往,系统建设将会越来越困难,甚至陷入不断推翻、重建的循环。
每个人都很忙,紧迫且繁重的任务容易使人陷入局部、短浅的思维方式,从而做出妥协式的决策
为了解决开发效率中代码冗余的问题,先对功能做一轮抽象,将所有公共功能抽象为服务。
服务虽然拆分了,但是数据库依然共用,这种“烟囱式系统”存在明显缺点:
不能忍了,必须对DB下手。所有持久化层相互隔离,由各个服务自己负责。另外,为提高系统的实时性,加入消息队列机制。新架构如下:
DB 完成拆分之后,各个服务采用异构技术:
数据库拆分也存在一些问题和挑战:比如跨库级联的需求,通过服务查询数据颗粒度的粗细问题。这里不做展开。
在做服务抽象的过程中,必然会出现公共服务,这类服务该谁来负责呢?
在单体应用时代,公共的业务功能经常没有明确的归属。最后要么各做各的,每个人都重新实现了一遍;要么是一个能力比较强或者比较热心的人,做到他负责的应用里面。在后者的情况下,这个人在负责自己应用之外,还要额外负责给别人提供这些公共的功能 -- 而这个功能本来是无人负责的,仅仅因为他能力较强,或者比较热心,就莫名地背锅(这种情况还被美其名曰能者多劳)。结果最后大家都不愿意提供公共的功能。长此以往,团队里的人渐渐变得各自为政,不再关心全局的架构设计。
要想解决这个问题,需要从组织架构层面予以支撑,将公共业务功能放到专职团队维护。所以开头提到,技术架构会影响到组织架构。
促销活动效果很好,引来了大量客户,突然系统挂了,一直返回502。经过登录服务器排查,对服务手工调用测试,发现是促销服务挂了,而所有服务都依赖于促销服务。排障就花了30分钟,没时间具体分析原因了,赶紧新建虚拟机,重新部署服务节点。前前后后一个小时过去了,损失了几十万。
微服务架构在带来便利的同时,也引入了新的问题,我们从软件生命周期来看:
工作久了就知道,系统发生故障是常态,不发生故障才是意外。所以大葱早就树立了 “Designing for Failure”的理念。我们知道
所以要降低故障带来的危害,就要从P和T入手。
降低故障发生概率:
减小故障影响:
很多公司把对分布式的观察系统叫做日志处理系统、告警系统、监控系统等,老实讲都是片面的,没有真正理解观察系统和监控系统字面用词区别下的本质理念区别。一个优秀的观察系统不单是日志处理、监控、告警这些单维的功能组件。它是基于日志数据、指标数据等基础数据并结合链路追踪技术做数据综合处理后形成的完备的无缝的观察平台。(参见《观察系统中的Logging,Metrics和Tracing》[2])
OpenTelemetry 基本定义了一个好的观察系统最后要做到的形态:终态就是实现Metrics、Tracing、Logging的融合,作为CNCF可观察性的终极解决方案.
一、Metrics 指标
各组件需要监控的指标不同
做一个大而全的监控系统来监控各个组件不大现实,扩展性也很差。一般做法:让各组件提供报告自己当前状态的接口(Metrics 接口),各接口输出的数据格式保持一致。这种接口基本都有开源组件,称为Exporter。例如Reids、Mysql 都有对应的 Metrics Exporter。
上图中,可以采用 Prometheus 作为指标采集器,Grafana 配置监控界面和邮件告警。
二、Tracing 链路跟踪
链路跟踪有两个核心概念。
1、 Span:指调用一个组件所经历的一段过程,也就是说,从请求组件开始,直到组件响应为止,在这段过程中会花费一定的时间,这是一个时间跨度,所以形象称其为Span。每个Span都带有一个可以唯一识别的ID号,称其为Span ID。
2、Trace:指从客户端发出请求,直到完成整个内部调用的全部过程,将这个过程称为一次追踪,Trace 就是这次追踪过程。每个 Trace 也带有一个可以唯一识别的ID,称为 Trace ID。由于组件之间会产生调用关系,那么每个Span也会出现依赖关系,因此每个Span都有对应的上级Span,称为Parent Span,用ParentID来表示当前Span所依赖的Parent Span。
技术实现上,可以写个 HTTP 请求的拦截器,每次服务调用会在 HTTP 的 HEADERS 中记录至少记录四项数据,同时异步发送调用日志到日志收集器中
这里额外提一下,HTTP 请求的拦截器,可以在微服务的代码中实现,也可以使用一个网络代理组件来实现(不过这样子每个微服务都需要加一层代理)。另外,还需要调用日志收集与存储的组件,以及展示链路调用的 UI 组件。关于链路跟踪的理论依据可详见 Google 的 Dapper。
链路跟踪只能定位到哪个服务出现问题,不能提供具体的错误信息。查找具体的错误信息的能力则需要由日志分析组件来提供。
三、Logging 日志分析
说到日志,网络安全从业人员马上就会想到 SIEM(security information and event management),目前最常见的就是 ELK 日志分析套件:
拆分成微服务后,出现大量的服务,大量的接口,使得整个调用关系乱糟糟的。经常在开发过程中,写着写着,忽然想不起某个数据应该调用哪个服务,或者调用了不该调用的服务,本来一个只读的功能结果修改了数据……
为了应对这些情况,微服务的调用需要一个把关的东西,也就是网关。在调用者和被调用者中间加一层网关,每次调用时进行权限校验。
使用网关有一个问题就是要决定在多大粒度上使用:
前面的组件,都是旨在降低故障发生的可能性。然而故障总是会发生的,所以另一个需要研究的是如何降低故障产生的影响。
最粗暴的(也是最常用的)故障处理策略就是冗余。一般来说,一个服务都会部署多个实例,这样一来能够分担压力提高性能,二来即使一个实例挂了其他实例还能响应。
冗余的一个问题是使用几个冗余?这个问题在时间轴上并没有一个确切的答案。根据服务功能、时间段的不同,需要不同数量的实例。比如在平日里,可能 4 个实例已经够用;而在促销活动时,流量大增,可能需要 40 个实例。因此冗余数量并不是一个固定的值,而是根据需要实时调整的。
一般来说新增实例的操作为:
操作只有两步,但如果注册到负载均衡或 DNS 的操作为人工操作的话,那事情就不简单了。想想新增 40 个实例后,要手工输入 40 个 IP 的感觉……
解决这个问题的方案是服务自动注册与发现。首先,需要部署一个服务发现服务,它提供所有已注册服务的地址信息。DNS 也算是一种服务发现服务。然后各个应用服务在启动时自动将自己注册到服务发现服务上。并且应用服务启动后会实时(定期)从服务发现服务同步各个应用服务的地址列表到本地。服务发现服务也会定期检查应用服务的健康状态,去掉不健康的实例地址。这样新增实例时只需要部署新实例,实例下线时直接关停服务即可,服务发现会自动检查服务实例的增减。
服务发现还会跟客户端负载均衡配合使用。由于应用服务已经同步服务地址列表在本地了,所以访问微服务时,可以自己决定负载策略。甚至可以在服务注册时加入一些元数据(服务版本等信息),客户端负载则根据这些元数据进行流量控制,实现 A/B 测试、蓝绿发布等功能。
当一个服务因为各种原因停止响应时,调用方通常会等待一段时间,然后超时或者收到错误返回。如果调用链路比较长,可能会导致请求堆积,整条链路占用大量资源一直在等待下游响应。所以当多次访问一个服务失败时,应熔断,标记该服务已停止工作,直接返回错误。直至该服务恢复正常后再重新建立连接。
当下游服务停止工作后,如果该服务并非核心业务,则上游服务应该降级,以保证核心业务不中断。比如网上超市下单界面有一个推荐商品凑单的功能,当推荐模块挂了后,下单功能不能一起挂掉,只需要暂时关闭推荐功能即可。
一个服务挂掉后,上游服务或者用户一般会习惯性地重试访问。这导致一旦服务恢复正常,很可能因为瞬间网络流量过大又立刻挂掉,在棺材里重复着仰卧起坐。因此服务需要能够自我保护——限流。限流策略有很多,最简单的比如当单位时间内请求数过多时,丢弃多余的请求。另外,也可以考虑分区限流。仅拒绝来自产生大量请求的服务的请求。例如商品服务和订单服务都需要访问促销服务,商品服务由于代码问题发起了大量请求,促销服务则只限制来自商品服务的请求,来自订单服务的请求则正常响应。
以下内容参考:《分布式服务架构:原理、设计与实战》[3]
第一部分介绍了从单体架构到微服务架构的变化过程中,引入的一些概念。这些概念,最终会被封装为产品,提供给开发人员使用。
J2EE是Java Enterprise Edition的简称,单体架构分为三个层级:
每个层次的多个业务逻辑的实现,会被放在同一应用项目中,并且运行在同一个JVM中。属于单体应用。尽管JEE支持WEB和EJB容器分离,但由于用户较少,大多数项目仍然部署在同一个应用服务器上并跑在一个JVM进程中。
J2EE 规范优点:全面、权威 + IBM 推广,但是加重了开发者使用的成本和负担,EJB2.0 大量使用XML配置,导致实现一个服务工作颇多,组件学习成本高,被称为超重量级的组件开发系统。
在J2EE开始流行但还没有统治地位时,Struts + Spring + Hibernate 成为了开源框架标配,直接干死了EJB。
这一时代的大多数企业里的SSH架构最终会被打包到同一个JEE规范的War包里,部署到Tomcat 容器。整个结构依然趋向于传统单体架构,业务逻辑仍然耦合在一个项目中。
SOA,面向服务的架构,几个特点:
SOA有两个主流实现方式:web service和ESB
通过WSDL定义的服务发现接口进行访问,利用SOAP协议进行通信(在HTTP通道传输XML的协议)。
每次看到XML,我都会想起图书馆的这本大部头,原以为XML语法简单没什么要学,没想到还有几百页的“高级教程”。
ESB:Enterprise Service Bus,企业服务总线。WebSphere 是ESB的一种实现。
出现的背景:企业已经有大量存量的信息化系统,在现有系统上增加新功能或者叠加新的服务化系统方法更可信。SOA 松耦合特性,正好应用于此。
所有服务在总线进行插拔,并通过总线的流程编排和协议转接能力来组合实现业务处理能力。
总结:两者面临的挑战
实现方式 | 存在的问题 |
---|---|
WebService | * 依赖中心化的服务发现机制 * 采用SOAP通信,基于XML格式,冗余太大,协议太重 * 服务化管理和治理设施不完善 |
ESB | * 体现了系统集成的便利性,通过统一的服务中线将服务组合在一起,并提供组合的业务流程服务 * 组合在ESB上的服务本身可能是一个过重的整体服务,或者是传统JEE服务 * ESB视图通过总线隐藏系统内部复杂性,但是系统内部复杂性仍然存在 * 对于总线本身的额中心化管理模型,系统变更影响的范围经常随之扩大 |
与WebService和ESB相比,不再强调服务总线和通信机制的多样性,利用RESTFul API和轻量级通信协议来完成。目前最主流的框架是 Spring Boot。
微服务特点:
在内网能看到大量关于gRPC/tRPC的帖子,第一反应是,RPC 不是很古老的技术了吗?
高中毕业那会儿,网吧开始出现在大街小巷,反恐精英的画面晃到人头晕。为了能在网吧多免费上会儿网,好朋友教会了我如何破解万象网管。破解的核心,就是在本地停掉RPC服务。
其实,RPC在1984年就出现了,Birrell 和 Nelson 当时在 ACM Transactions on Computer Systems 的论文《Implementing remote procedure calls》已经对 RPC 做了经典的诠释。(参见:[《远程过程调用(RPC)详解》](https://waylau.com/remote-procedure-calls/ "《远程过程调用(RPC "《远程过程调用(RPC)详解》")详解》"))
RPC 全称是远程过程调用,反义词就是“本地过程调用”。
先来看看本地过程调用,用C写printf("Hello world!")
时,printf 函数由链接器从库中提取出来,链接器再将它插入目标程序中。printf 是一个位于用户代码与本地操作系统之间的接口。虽然 printf 中执行了系统调用,但它本身依然是通过将参数压入堆栈的常规方式调用的,堆栈使用的是本地内存空间,程序员并不知道 printf 具体做了什么。
而远程过程调用的作用,就是让程序员无感知的调用远程服务器上的printf。这个实现过程,类似下图:
将本地 printf 替换为远程版本的动作,称为客户存根(client stub)。
RPC作为C/S模式的通信方式,一次完整的通信过程包括10个步骤:1.客户过程以正常的方式调用客户存根;2.客户存根生成一个消息,然后调用本地操作系统;3.客户端操作系统将消息发送给远程操作系统;4.远程操作系统将消息交给服务器存根;5.服务器存根调将参数提取出来,而后调用服务器;6.服务器执行要求的操作,操作完成后将结果返回给服务器存根;7.服务器存根将结果打包成一个消息,而后调用本地操作系统;8.服务器操作系统将含有结果的消息发送给客户端操作系统;9.客户端操作系统将消息交给客户存根;10.客户存根将结果从消息中提取出来,返回给调用它的客户存根。
对RPC的实现,也经历了几个阶段。
第一代
第二代
第三代(1998)
第四代(2006)
首先恭喜你,能够坚持看到这里。现在请你闭上眼睛,看看自己还能记住多少?
上图是Edgar Dale在1954年提出的学习金字塔(Cone of Learning)。可以看到,仅仅通过“读”学到的内容留存率非常低,即便有了前面的概念引入和工具介绍,要想深入理解与记忆,还需要来些“实践仿真”。
这里推荐读者阅读《架构探险:轻量级微服务架构》[4],书中详细介绍了如何从零搭建:
笔者也会将这本书作为本次文章的抽奖礼品,请关注文章末尾了解抽奖详情。
参考:Docker容器监控之 CAdvisor+InfluxDB+Granfana[5]
基于 cAdvisor + InfluxDB + Grafana,对时序数据进行收集 + 存储 + 分析展示。
时序数据分析:measurement、tag、field
新建 docker-compose.yml文件
version: '3.1'
volumes:
grafana_data: {}
services:
influxdb:
image: tutum/influxdb:0.9
restart: always
environment:
- PRE_CREATE_DB=cadvisor
ports:
- "8083:8083"
- "8086:8086"
volumes:
- ./data/influxdb:/data
cadvisor:
image: google/cadvisor
links:
- influxdb:influxsrv
command: -storage_driver=influxdb -storage_driver_db=cadvisor -storage_driver_host=influxsrv:8086
restart: always
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:rw
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
grafana:
user: "104"
image: grafana/grafana
user: "104"
restart: always
links:
- influxdb:influxsrv
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
environment:
- HTTP_USER=admin
- HTTP_PASS=admin
- INFLUXDB_HOST=influxsrv
- INFLUXDB_PORT=8086
- INFLUXDB_NAME=cadvisor
- INFLUXDB_USER=root
- INFLUXDB_PASS=root
启动
docker-compose config -q # 验证docker-compose.yml文件配置,当配置正确时,不输出任何内容,当文件配置错误,输出错误信息。
docker-compose up -d
docker ps
应用日志中心:logstash + ElasticSearch + Kibana => 收集 + 存储 + 分析展示
网上有很多资料,可以自己搜索。
参见:《如何手撸一个较为完整的RPC框架》[6]
指标接口、链路跟踪、可观测性、服务注册发现等组件以及熔断、限流等功能都需要在应用服务上添加一些对接代码。如果让每个应用服务自己实现是非常耗时耗力的。因此,现在有众多的微服务框架,基于 DRY 的原则,可以将与各个组件对接的代码和另外一些公共代码抽离到框架中,所有的应用服务都统一使用这套框架进行开发。
使用微服务框架可以实现很多自定义的功能。甚至可以将程序调用堆栈信息注入到链路跟踪,实现代码级别的链路跟踪。或者输出线程池、连接池的状态信息,实时监控服务底层状态。
但是,使用统一的微服务框架有一个比较严重的问题:框架更新成本很高。每次框架升级,都需要所有应用服务配合升级。当然,一般会使用兼容方案,留出一段并行时间等待所有应用服务升级。但是如果应用服务非常多时,升级时间可能会非常漫长。并且有一些很稳定几乎不更新的应用服务,其负责人可能会拒绝升级…… 因此,使用统一微服务框架需要完善的版本管理方法和开发管理规范。
在微服务框架之外,还有另外一种选择。在1.4.2 网关这一节我们提到过,另一种抽象公共代码的方法是直接将这些代码抽象到一个反向代理组件。每个服务都额外部署这个代理组件,所有出站入站的流量都通过该组件进行处理和转发。这个组件被称为 Sidecar。
Sidecar 不会产生额外网络成本。Sidecar 会和微服务节点部署在同一台主机上并且共用相同的虚拟网卡。所以 sidecar 和微服务节点的通信实际上都只是通过内存拷贝实现的。
Sidecar 只负责网络通信。还需要有个组件来统一管理所有 sidecar 的配置。在 Service Mesh 中,负责网络通信的部分叫数据平面(data plane),负责配置管理的部分叫控制平面(control plane)。数据平面和控制平面构成了 Service Mesh 的基本架构。
图片来自:Pattern: Service Mesh[7]
Sevice Mesh 相比于微服务框架的优点在于它不侵入代码,升级和维护更方便。它经常被诟病的则是性能问题。即使回环网络不会产生实际的网络请求,但仍然有内存拷贝的额外成本。另外有一些集中式的流量处理也会影响性能。
微服务不是架构演变的终点。往细走还有 Serverless、FaaS 等方向。另一方面也有人在唱合久必分分久必合,重新发现单体架构……
《一文详解微服务架构》: https://www.cnblogs.com/skabyy/p/11396571.html
[2]《观察系统中的Logging,Metrics和Tracing》: https://segmentfault.com/a/1190000039082442
[3]《分布式服务架构:原理、设计与实战》: http://product.dangdang.com/25118931.html
[4]《架构探险:轻量级微服务架构》: http://product.dangdang.com/11123424484.html
[5]Docker容器监控之 CAdvisor+InfluxDB+Granfana: https://blog.csdn.net/weixin_43847283/article/details/122624859
[6]《如何手撸一个较为完整的RPC框架》: https://juejin.cn/post/6992867064952127524
[7]Pattern: Service Mesh: http://philcalcado.com/2017/08/03/pattern_service_mesh.html
公众号名为 SecOps 急行军,创办于笔者即将从某金融单位离职进入互联网公司期间。这里的 SecOps 不是安全运营,而是安全运维。因为过去的工作经历让我意识到,很多安全问题,从运维的角度来解决,其实能事半功倍,两者需要进一步融合。进入互联网公司后,我负责了运维开发团队(SRE),进一步坚定了这个观点。