需要了解的概念

  • DNAT映射:网络报文,目的地址转换。
  • 胖客户端:像Cassandra 客户端、Kafka客户端,客户端感知中间件拓扑,即客户端感知中间件部署、均衡,把请求发给指定的中间件服务器。
  • VPC:用户的私有网络环境。
  • VPC Peering:多个网段不冲突的VPC之间网络打通的方式,可跨用户。
  • 三方厂家:非公有云厂商的PaaS供应商。
  • 中间件用户:需要在公有云上运行中间件的公有云用户。

前言

现在,在公有云上买rediskafka这类组件已经变得非常普遍,由公有云提供的中间件往往能给你带来良好的体验,现在中间件在公有云上的交付模式大致分为三种。我对
Pulsar较为熟悉一些,大部分图例以消息中间件Pulsar举例。本文中成本指用在云服务或三方厂商上的成本,指人力成本时,会注明人力成本。

image-20210424145257826

公有云模式

公有云提供的中间件有得天独厚的优势,主要优势在以下几点

低廉的成本

  • 和三方厂家相比,对中间件用户来说,云厂商自运营的价格要低于三方厂家。
  • 和租户自运维相比,对中间件用户来说,云厂商价格略高一点点,但省去了大量的运维人力。

使用方便

云厂商可以把中间件的ip地址申请在你的vpc内,对任何应用程序来说,连接都是最方便的。无论是容器化部署、虚拟机部署、还是本vpc和其他vpc
peering打通的场景,都可以通信。

监控、运维系统对接

得力于云厂商的积累,能够方便地和云厂商的告警、统计系统对接,接收告警通知和报表等。甚至可以在手机app上查看云中间件的监控指标。

安全

  • 公有云有专门的团队时刻关注中间件安全漏洞,及时打上补丁
  • 有些白帽组织会在发现漏洞并公开发布前通知一些大的使用者,大的厂家可以在漏洞公布前提前预防

主要的劣势有

  • 云厂商提供的中间件有限,不是所有中间件的所有版本都支持
  • 即使业务有强烈诉求,也很难在中间件上做定制修改

租户自运维模式

租户自运维,就是用户自己部署中间件维护,仅利用云上的基础设施。主要的优势有

  • 可以在中间件上做定制修改
  • 可按任意规则进行部署,节约成本
  • 可部署任意版本中间件,比如代码一定要Kafka 0.11版本,公有云上该版本已停止售卖,代码不能改,那就只好自己部署Kafka的该版本了。

主要的劣势有

  • 和运维系统对接,需要人力成本。因为业务本身也需要对接运维系统,这部分人力成本较低,常见的困难点在于中间件的技术栈和团队不符(如Java团队尝试在go语言开发的中间件上对接运维系统)。
  • 中间件的网上运维、问题处理需要很大的人力成本,且对人员技术水平要求高。
  • 安全问题、漏洞,可能处理不及时,存在安全隐患。

三方厂家模式

三方厂家的主要优势有

  • 安全性方面,如果是社区主导者,有些白帽组织可能会提前告知发现的漏洞
  • 如果是社区的主导者,可以将您的新的合理需求优先级提高开发。

主要劣势

  • 运维上无法和公有云打通,三方厂家需要自己构建一套运维系统并通知到客户,对客户来说,也要适配三方厂家的运维方案。
  • 成本比公有云模式高
  • 易用性上面,网络是三方厂家的痛点,这点值得单独在下一小节详细描述

三方厂家网络模式

三方厂家最难做的就是,如何在厂家用户中间件用户隔离的情况下,提供中间件的接入点,可行的方式有以下几种

VPC Peering打通模式

Peering可以说是最简单的网络打通模式,可以给用户不错的私网体验(如果走公网,数据至少要是tls加密的,不然会有数据在公网上被人监听的风险)。只需要跟用户规划一个不冲突的网段。

image-20210424110409903

这个网段不需要很大,一般26、27就可以满足要求,27的网段已经可以部署30台虚拟机了,值得一提的是,这种打通模式下,一般不会选择容器化部署,都是虚拟机部署,原因有二

  • 如果客户也使用k8s容器化部署,两个容器化集群互通,对网设的要求很高,限制很大。
  • 仅仅部署一个中间件的话,加上k8s集群的部署,成本占比太高

VPC Peering的主要挑战有

  • 自动化:自动化较为困难,需要考虑如何获取尽量少的客户权限,自动化完成整个流程。
  • 成本:Peering的模式下,三方厂商几乎只能针对每个用户的集群单独Peering,很多地方无法均摊成本。想要客户购买,需要对客户有很强的吸引力。
  • 网络规划受限,路由规则配置复杂:如果客户不仅仅和三方厂家建立了Peering关系,又和其他VPC建立了Peering关系,这三方网络不能冲突,还要互相配置路由规则。这需要进行细致的网络规划,毕竟网络一旦冲突,解决的代价很大。

LB接入模式

LB接入模式,在三方厂家模式下,只能通过公网的方式接入,一般给用户提供域名或ip地址接入,域名使用的较多,像华为云设备接入服务、Azure
物联网服务等都是提供域名,主要的原因

在EIP资源有限的情况下,将部分用户的域名解析成一个地址,既能节约EIP资源,也能在一些基础设施上共用资源,节约成本(如共k8s、共数据库、共Bookkeeper等)

image-20210507194200489

LB接入的限制是,中间件需要支持可负载均衡访问,如果中间件的客户端是有状态的胖客户端(如Pulsar、Cassandra),需要中间有一层无状态的代理。

华为云VPCEP模式

VPCEP,VPC终端节点,是华为云的云服务,可以将一个VPC的地址,映射到另一个VPC的地址,在客户端的视角来看,本质上是做了一次DNAT地址映射。

image-20210424230158506

VPCEP的主要限制是连接中间件经过了DNAT映射。需要对应中间件支持DNAT的方式访问。另一方面其他云上不一定有该能力。三方厂商一般也希望做一个统一的方案来适配所有云厂商,所以目前使用的并不是很多。

三方厂商模式的劣势主要是

  • 公网的方式下,时延高,难以满足有低时延要求的客户,且带宽费用也占比很大
  • Peering模式,成本高,费用贵

并且如果公有云厂商没有提供新的用于用户和用户之间打通网络的方案,那么对三方厂商来说,在接入方面,恐怕很难再有所突破了。

注:这里的网络打通三种方式不限于三方场景和用户之间,还可以扩展到用户和用户之间网络打通进行通信,这里不再扩展了。

建议选型流程

中型和长尾用户可按照下图流程进行选型。

对于大型用户,可能会维护自己的核心中间件,紧密联系自己的业务,在上面做定制开发,不但降低成本,并且提升业务竞争力。非核心中间件,也可参照下图流程进行选型。

image-20210507195340204

总结

公有云 三方厂商 自建
机器成本
处理测试、网上问题工作量
运维系统对接开发量
接入灵活性
安全

大型系统中的证书管理

随着安全的要求,现在我们在越来越多的通信中使用TLS加密。下图是一个微服务架构下数据流向的例子

cert-manager1

  • 蓝色部分,即和三方交互时需要TLS加密认证
  • 红色部分,各个微服务、消息中间件等通信需要TLS加密认证
  • 绿色部分,各个微服务和存储层通信也需要TLS加密认证

安全上对我们的要求逐步变化为,仅蓝色使用TLS=》蓝色和红色使用TLS=》全部使用TLS加密

证书管理的必要性

从安全的角度上来说,我们最好能支持证书的更换和热加载。如果您的业务当前使用加密的场景不多,可能暂时看不到证书管理的意义。但是当你在各个方面使用TLS更加频繁之后,会发现证书管理可带来如下好处:

  • 可以通过抽象出场景,通过场景和证书的关联联系,在各个地方通信使用的证书,可以统一更换。
  • 统一提供证书过期告警等功能
  • 统一提供证书的变更通知,通知到各个实例

以我在工作中接触到的两个基础PAAS平台,都有证书管理的功能,可见证书管理的必要性。

PS: 开源组件大多都拥有证书配置能力,没有可对接证书管理的能力,但这个能力很难贡献给社区,需要自己开发。

证书管理概念

在TLS会话中,从依赖的证书文件角度来看,可以分为加密流程和验证流程。

加密证书

TLS加密流程的证书,包含证书链文件和密钥

验证证书

TLS验证流程的证书,仅包含证书链文件

拆分为加密流程和验证流程的合理性

这使得加密流程证书和验证流程证书可以互相独立的替换,更方便在大型场景下复用证书。

让我们来假设如下的场景:

cert-manager2

客户A、客户B、客户C、客户D的验证流程证书自然不相同,但服务跟客户交互的时候,使用的加密流程证书确实同一份。如果将两个阶段的证书合一,那么在更换证书的时候,就需要更新4份数据,当你有1000名用户的时候,这个数字将会是1000,这对于存储和应用程序来说都是不小的冲击。

Scene

Scene是在一个会话中,代表会话和请求证书、验证证书的绑定关系。Scene和请求证书、验证证书都是1:1的关系。这使得我们不仅仅可以修改证书文件,也可以对TLS会话中使用的证书进行修改。在证书无法复用,且证书绑定了多个场景的时候,针对单个场景修改其绑定的证书。

以上图作为例子,假设客户D有特殊的要求,要求加密流程使用特定的证书或密钥,我们就可以将客户D的场景绑定到客户D独有的加密证书

多集群管理

如果证书管理需要管理多个集群,那么证书和Scene前面可以加上层级来隔离,如环境、集群等。

对小型系统的建议

如果规模不大,且TLS场景有限,需要考虑一下有无拆分加密证书和验证证书的必要,可以合一,应用程序直接以合一的证书id来关联,而非场景id。虽不方便复用,但大大降低了复杂性。

证书管理的功能

cert-manager3

证书管理场景

设定一个TLS会话

cert-manager4

使用TLS会话

这要求应用程序持久化场景信息

cert-manager5

组织架构相关

大型系统下,证书管理是一个必须的组件,且一定是由团队最底层的组织架构承接。如若不然,那么由底层组织架构维护的组件,因为依赖关系,无法基于证书管理来统一实现证书的更换和过期告警。除非不基于证书管理自己构筑一套能力。

TL;DR

随着组件和使用加密场景的不断扩大,证书管理是一个必须的组件,通过抽象出场景的概念来复用证书,通过变更通知在微服务模式下快速更换所有微服务实例上的证书,并提供统一的证书过期告警功能来提醒管理员更换证书。

前言

之前在InfoQ的《华为云物联网四年配置中心实践》文章中分享了业务配置中心。

本文讲述业务配置中心(下文简述为配置中心)的关键技术和实现方式。华为云物联网平台按照本文的实现方式实现了一个业务配置中心,该配置中心2020年1月上线,平稳运行至今。

概念

运维配置

和用户无关,通常为集群界级别的配置,程序只会进行读取,如数据库配置、邮箱服务器配置、网卡配置、子网地址配置等。

业务配置

作为SaaS 服务,每个用户在上面都有一些业务配置。如用户的证书配置、用户服务器的流控配置等,这些业务配置相对运维配置来说更加复杂,且可能会有唯一性限制,如按用户 id 唯一。这部分配置数据一般由用户操作触发,代码动态写入,并且通知到各个微服务实例。通常,我们希望这些配置能在界面展示,且支持人为修改。上述逻辑如果由各微服务自己实现,会存在大量重复代码,并且质量无法保证。我们希望由一个公共组件来统一实现这个能力。开源或体量较小的项目就不会选择依赖一个配置中心,而是直接通过连接数据库或etcd来解决问题

env

代表一个部署环境。

cluster

代表环境下的集群。常见于单环境下蓝绿发布,蓝集群、绿集群、金丝雀集群等。

配置

配置名称,如用户证书配置、用户流控配置等。

Key

配置的唯一键,如用户id。

Value

配置唯一键对应的值。

配置中心设计梗概

业务配置特点

  • 虽然业务配置写入可能存在并发,但并发量不大,频率较低。
  • 业务配置常常以用户为id,单集群用户量有限,一般不超过5万。

配置中心要解决的问题

business-config-center-impl1

设计要点

  • 单配置要求有配置id,每个id上通过version的乐观并发控制来解决多版本冲突问题
  • 通知不追求可靠,应用程序和配置中心断链无法接收通知的场景下,通过定期同步数据来保证数据的可靠
  • 支持Schema的变更,因Schema变更不频繁,也采用version的乐观并发控制来解决多版本冲突问题

通知是否包含消息内容

我认为应该只通知Key,具体的数值让应用程序再去配置中心查询。仅通知Key实现简洁易懂。同时通知Key&Value需要多考虑定期同步和通知两条通道并发,可能引起的竞态冲突。

配置中心业务流程

本小节描述业务配置中心的所有业务流程,并试图从交互中抽象出与具体实现无关的接口

配置的增删改查

business-config-center-impl2

配置值的增删改查

business-config-center-impl3

定期同步

分布式场景下,通知有可能无法送达,如程序陷入网络中断(或长gc),通知消息送达超时,待程序恢复后,数据不再准确。因此需要对数据做定期同步,提高可靠性。

business-config-center-impl4

同步过程中,仅仅请求交互id和version,避免传输大量数据。应用程序接收到需要同步的数据后:

  • 删除操作,触发删除通知,从本地缓存中移除数据。
  • 添加、修改操作,向配置中心查询最新数据,触发通知并写入本地缓存。

服务启动

服务启动也可看做是一个同步的流程,只是需要同步大量的数据添加。为了避免向配置中心频繁大量的请求,引入批量操作来减轻压力

business-config-center-impl5

限制

该配置中心设计思路依赖客户端可把数据全量放入到内存中,如用户量太大,则不适合采用这种模式。

注:一个节省内存的思路是,内存中只放置全量的id和version,数据只有当用到的时候再去查询。这个思路要求配置中心持久化一些老旧数据以供以下场景的查询使用

  • 业务流程中,需要使用该配置值的。

  • 回调业务程序修改的时候,需要提供旧值的。

除此之外没有任何区别。

业务配置抽象实现

从上述描述的业务场景,我们抽象出业务配置中心的交互接口和抽象实现。接口的Swagger Yaml已上传到Github:https://gist.github.com/hezhangjian/68c9c2ecae72cc2a125184e95b0a741e

配置相关接口

  • 提供env、cluster、配置名称、配置Schema、配置版本号添加配置
  • 提供env、cluster、配置名称删除配置
  • 提供env、cluster、配置名称、新Schema、新Version来修改配置
  • 提供env、cluster、配置名称来查询配置

配置值相关接口

  • 提供env、cluster、配置名称、Key、Value来添加配置值
  • 提供env、cluster、Key、ValueVersion(可选)来删除配置值
  • 提供env、cluster、Key、Value、ValueVersion(可选)修改配置值
  • 提供env、cluster、Key查询配置值
  • 根据env、cluster、应用程序当前的配置数据来做定期同步
  • 根据Key列表批量查询配置值

通知相关接口

  • 通知某env某cluster下,配置项中的一个Key发生变化,新增、修改或是删除。可选方式有HTTP长链接(Inspired by Apollo)、Mqtt、WebSocket等。

配置中心存储层抽象实现

配置中心存储层需要存储配置配置值数据,支持UpdateByVersion,且需要捕捉数据的变化,用来通知到应用程序

服务发现抽象实现

为了使应用程序连接到配置中心,需要一个发现机制可以让应用程序感知到配置中心的地址。高可用的方式很多,如K8s发现、ZooKeeper、Etcd、ServiceComb、业务环境变量注入ELB地址(ELB后端挂载配置中心的地址)等。

抽象总结

business-config-center-impl6

根据这个抽象,我们可以进行关键技术点选型,来实现业务配置中心。

配置中心实现

华为云物联网配置中心实现

business-config-center-impl7

  • env+cluster+config组成数据表的名称
  • 一个key、value对应一行数据

另一种实现方式

只要实现上述接口和抽象能力,都可以实现业务配置中心,也可以这么实现

business-config-center-impl8

  • env+cluster+config+key 组合成etcd的key
  • 一个key、value对应一个键值对

又一种实现方式

当然也可以

business-config-center-impl9

  • env+cluster+config+key 组合成RocksDB的key
  • 一个key、value对应一个键值对

一句话结论,可以在拷贝镜像文件的时候,通过如下命令指定user来压缩dockerfile的体积,避免把指定的文件在dockerfile中计算两次。

为什么要指定User?

  • 往往,我们会因为安全的要求,不允许使用root用户运行程序。
  • 像ElasticSearch这个开源组件要求不能用root用户运行,其实也是出于安全的原因
1
COPY --chown=sh:sh source /opt/sh

效果展示

先使用dd命令创建1GB的测试文件

1
dd if=/dev/zero of=testfile bs=1024 count=1048576

测试基础镜像ttbb/base:latest,大小439MB

1
2
docker images|grep 'ttbb/base'|grep latest
ttbb/base latest bacdb9e7b5f4 2 weeks ago 439MB

优化前DockerFile

1
2
3
4
5
FROM ttbb/base

COPY testfile /opt/sh/testfile

RUN chown -R sh:sh /opt/sh/testfile

大小

1
1280d315e09d        31 seconds ago      2.59GB

可以看到testfile计算了两次,大小达到了2G多。

优化后DockerFile

1
2
3
FROM ttbb/base

COPY --chown=sh:sh testfile /opt/sh/testfile

大小

1
115b68bc4db8        21 seconds ago       1.51GB

testfile仅计算一次,仅使用1.5G。

Raft主要使用了重叠的大多数技术来保证算法的安全

Raft首要追求的是可理解性

Raft使用数个技术来提升可理解性。包括

  • 问题分解:主备选举、日志复制、安全性
  • 尽量减少状态空间(相比Paxos,Raft减少了不确定性)

Raft新颖的特性

强leader

日志文件只单向传输,简化状态

leader选举

Raft使用随机定时器来选举leader。只添加了很小的机制,却能简单、快速解决冲突

Membership变更

Raft的Membership变更机制使用joint consensus方法,在变更过程中,两个不同配置的大多数 重叠。这使得在集群成员变更时,也能正常处理请求

复制状态机

补图

保证复制状态机的一致,也就保证了数据的一致

一致性算法拥有如下的典型属性

  • 在非拜占庭场景下,保证了正确性。包括 网络延迟、分区、丢包、乱序等
  • 当大多数节点在线的时候,功能可用
  • 不依赖时间来保证日志的一致性。错误的时钟和极大地消息延时,在最差的场景下,可能会导致一致性问题
  • 在最常见的场景下,当一轮大多数节点反悔的时候,就能完成一个命令。小部分节点响应缓慢并不影响系统的整体性能。

Raft协议

简述

Leader选举

新的leader必须在已存在的leader宕机后选出

日志复制

leader必须从客户端哪里接收日志请求,复制到整个集群,迫使其他人达成一致

安全

Raft的安全属性关键。如果任何服务器将一个entry log复制到状态机中,那么其他任意服务器都不能在相同的log index上放置不同的命令。

安全的详细内容

Election Safety 选举安全

每一个任期内,至多只会有一个leader

Leader仅追加

leader不会覆写或者删除已存在的entry,只会追加新的entry(todo 待确认,是写入的,还是commit的,从leader可能是一个老的节点来说,这里应该说commit的更为恰当)

Log Matching 日志匹配

如果两个日志具有相同的任期值和相同的index,那么直到这个index之前的日志都是一样的

Leader Completeness Leader完整性

如果一个日志在一个给定任期内提交了,那么这个日志会一直存在,存在在任何高任期的Leader之中

State Machine Safety 状态机安全

如果服务器已在其状态机上将给定索引的日志条目应用于其状态机,则其他服务器将永远不会对同一索引应用不同的日志条目

Raft基础

Raft将时间切分成任期时长间隔的任期。Raft保证一个任期内至多只有一个Leader。任期可以称为是Raft中的逻辑时钟。每个服务器之间都会互相传播任期值。

Leader选举

Raft使用随机的选举时间来保证分裂投票场景少见并快速解决。将选举超时设定为一个范围。

Raft的作者们考虑过使用不同的Rank值,当分裂投票的时候,Rank值高的优先成为主节点,但在可用性方面有细微的问题。Rank值低的节点需要超时才能成为新的leader,这个时间间隔如果太短,会破坏已有的选举,集群太过敏感)

最终认为随机的措施更明显、更易懂

日志复制

Leader来决定何时将日志提交到状态机是安全的,叫做committed提交。Raft保证提交过的entry都是持久化的,然后最终会被所有的状态机执行。

只有当前的Leader在任期内,然后将其复制到大多数节点,才算做committed!(这里有和仅仅复制到大多数节点有着重要的区别)然后这里会将之前的日志提交。

Leader每次发送AppendEntries RPC请求时,确认在这之前的日志和从节点完全相同。

Raft可以accept、replicate、应用新的日志记录。在正常场景下,经过一轮大多数RPC调用,就可以复制完成。

安全

假如,当leader提交数个日志的时候,follower不可用,然后他当选了leader之后,提交的日志把之前提交的日志覆盖了怎么办?

这里在选举当选leader的上面加了个限制,保证了之后的leader包含了之前所有已提交的entry。

选举限制

Raft使用投票阶段来防止一个没有之前提交过日志的候选者当选leader。候选者必须联络大多数节点才能当选,这就意味着提交过的entry一定在其中的一个服务器中。

提交之前任期的entry

leader不能立刻得出结论:之前任期的日志复制到大多数节点就已经算commit了。

image-20210325211802223

  • a S1是leader,然后部分复制了日志2
  • b S1宕机,S5接受了S3和S4的投票当选了任期3的leader,在index2接受了不同的entry
  • c S5宕机,S1重启,当选了leader,继续复制
  • d S1宕机,S5重启,然后用任期3的日志覆盖了其他节点
  • e 然而如果S1在宕机前,把日志覆盖到大多数节点,那么S5就不能当选leader了

为了避免上图的问题,Raft绝不将复制的数量当作commit 日志的依据。只有当前任期下的entry log通过复制数量来计算。一旦当前任期的entry被提交,那么之前所有的entry都被间接commit了。

安全性保证

我们用反证法证明一旦Leader Completeness Property没有满足,我们就会推断出一个矛盾。假设任期T的Leader提交了一个log entry在任期T,但是这个log entry没有被将来一些任期的Leader拥有。假设有一个没有包含这个entry的最小的任期U的Leader,Leader U没有存储这个entry

    1. 在选举的时候,提交的entry必须不在leader U的日志中(leader从不删除或复写日志)
    1. Leader将这个entry复制到了集群中的大多数节点,并且leader U接收到了集群中大多数节点的投票。至少有一个服务器,即从leader T哪里接受了entry,并且给U投票。这是达成矛盾的关键
    2. voter 必须在接收leaderT的entry之前给U投票。否则它就要拒绝T的写入请求
    3. 当voter给U投票的时候,它始终持久化着这个日志,因为每个中间的leader都包含这个entry,leader不会删除这个entry,除非冲突,follower也不会删除这个entry
    4. voter给U投票,所以U的日志必须至少和voter的一样新,这就达成了第一个冲突
    5. 首先假设,如果voter和U都有同样的上一次log的任期,U的日志至少和vote一样。矛盾,因为最初假设U没有这个log,而voter有。
    6. 否则,leader U的上次log任期比voter的大。此外,它比T大,因为选民的上一个log term至少为T(其中包含来自T的提交entry)。 创建leaderU的最后一个log term的较早的领导者必须在其日志中包含已提交的条目(通过假设)。 然后,通过Log Matching Property,leaderU的日志还必须包含已提交的条目,这是矛盾的
    7. 这就完成了矛盾的证明。比T任期大的leader一定包含了任期T内提交的entry

时间和可用性

Raft可以选举并维持一个稳定的leader,只要系统满足如下的时间限制条件

1
broadcastTime << electionTimeout << MTBF

broadcastTime是进行一个并行rpc到所有服务器来回的平均时间。MTBF是单个服务器故障的平均时间。

广播时间必须比选举时间小一个量级,所以leader放心的发送心跳消息,维护自己的follower。加上随机的选举时延,这个不等式也让选票分裂变得不可能。 如果广播时间和选举时间差不多,选举leader不稳定。

选举超时应该比MTBF小几个数量级,要不然选举的leader就不稳定。
broadcast的时间差不多在0.5ms到20ms
选举超时应该在100ms到500ms。
典型的服务器MTBF时间应该在数月或以上

集群成员变更

成员变更的时候,中途必须没有两个相同任期的leader。不幸的是,任何将服务器们直接从老配置变换到新配置都是不安全的。不可能一次性地原子性地把所有服务器的配置变更,所以中集群在中途可能会分裂为两个多数派。
为了保证安全性,集群成员变更必须使用两阶段的方式。有很多种方式实现两阶段提交。例如,一些系统使用第一次提交来禁用旧的配置,使得旧的配置无法接受客户端的请求,然后第二次操作启动新的配置。在Raft中,集群首先切换到一个过度的配置,叫做joint consensus。一旦joint consensus被提交,系统接下来过渡到新的配置。joint consensus结合了新老配置

  • Log entry在两种配置下都会复制。即新机器和老机器都会复制entry
  • 不管是老配置还是新配置,都有可能当选leader
  • Agreement(协议,包括选举和entry提交)需要老配置和新配置多数派都确认
    补图 Figure11
    集群配置通过复制日志中的特殊entry来进行存储、通信。

上述流程有三个问题
第一个问题是,新的服务器可能初始没有存储任何log entry。如果现在添加到集群中,会花费一些时间来跟上集群的数据,这中间有可能无法commit新的log entry。为了避免可用性的gap。Raft在配置变更之前引入了一个额外的阶段,新的服务器首先
第二个问题是,cluster的leader可能不是新配置中的服务器。这个场景,leader的变化发生在新配置提交的时候。
第三个问题是,移除的服务器可以打乱整个集群。这些服务器接收不到心跳,这些服务器会超时然后启动新的选举。他们将使用新的任期发送RequestVote RPC,会导致当前的leader变为follower。新的leader最终会被选举,但是移除的服务器将会再次超时,重复整个过程,最终导致集群较差的可用性。
为了解决这个问题,Server当认为有leader存在的时候,会忽略RequestVote请求。如果服务器在选举超时前接收到RequestVote RPC请求,它并不会更新它的任期或是给予它的投票。这并不影响正常的选举(每个服务器在选举之前等待最小超时时间)。并且,这有助于避免移除的server破坏选举:如果一个leader可以发送心跳到他负责的集群中的大多数节点,他将不会被更高任期的节点罢免。

成员变更过程中如果发生Failover,老Leader宕机, Cold,new中任意一个节点都可能成为新Leader,如果新 Cold,newLeader上没有 日志,则继续使用Cold ,Follower上如果有 Cold,new 日志会被新Leader截断,回退到 Cold,成员变更失败;如果新Leader上有 Cold,new日志,则继续将未完成的成员变更流程走完。

新成员先加入再同步数据,成员变更可以立即完成,并且因为只要大多数成员同意即可加入,甚至可以加入还不存在的成员,加入后再慢慢同步数据。但在数据同步完成之前新成员无法服务,但新成员的加入可能让多数派集合增大,而新成员暂时又无法服务,此时如果有成员发生Failover,很可能导致无法满足多数成员存活的条件,让服务不可用。因此新成员先加入再同步数据,简化了成员变更,但可能降低服务的可用性。

新成员先同步数据再加入,成员变更需要后台异步进行,先将新成员作为Learner角色加入,只能同步数据,不具有投票权,不会增加多数派集合,等数据同步完成后再让新成员正式加入,正式加入后可立即开始工作,不影响服务可用性。因此新成员先同步数据再加入,不影响服务的可用性,但成员变更流程复杂,并且因为要先给新成员同步数据,不能加入还不存在的成员。

日志压缩

每当有新的操作发生的时候,Raft的日志就会增长,然而在实际的系统中,日志并不能无边界地增长。
快照是最简单的压缩日志的方式。在快照中,整个系统的状态写入到持久化存储的快照中,然后在这之前的日志都可以丢弃。
todo 补图
其他方式,像日志清理或lsm树。在数据的一部分子集上面执行,它们均摊了压缩日志的消耗。

Leader创建snapshot,再分发给follower。有如下两个缺点
第一,Server必须选择何时进行快照,如果服务器快照进行地太频繁,将会浪费磁盘带宽和磁盘energy。如果快照太不频繁,会浪费磁盘的存储空间,然后增加了重放日志所需的时间。如果阈值设置地大,时间周期长的话,磁盘开销小。
第二,写快照会消耗较大的时间,我们不希望这个操作延迟了正常的操作。方案是使用Copy on write技术,这样子在不影响snapshot写入的情况下,集群可以接受新的更新。

客户端的交互

Raft实现了线性化的语义。Linearizable semantics。像es那样使用version,是达不到线性化的语义的。
在读取数据的时候需要额外的措施来保证线性化的语义。首先,leader必须知道最新有那些entry已经提交。Leader Completeness Property 保证了leader有所有的committed entries,但是在任期的开头,可能并不知道那些entry已被提交。(为了确认,可以发空请求来commit数据)
通过向大多数节点来发送心跳,来保证读请求的返回的是最新的。这里就依赖了前面所说的时钟。依赖时钟来实现安全。

性能

todo 补图
随机杀死leader,重新选举最短时间刚好是leader选举超时的一半,因为心跳超时时间刚好是选举超时的一半。

实现

InstallSnapshot RPC接口

由leader调用,发送snapshot的一部分到从节点。Leader总是按顺序发送chunk

参数

  • term leader的任期
  • leaderId 使得follower可重定向client的请求
  • lastIncludedIndex snapshot最后包含的index号
  • lastIncludedTerm snapshot最后包含的最后一个任期号
  • offset chunk在整个snapshot中的offset
  • data[] 原始数据
  • done 如果是last chunk则为true

返回体

term 当前任期,使得leader可以根据这个结果判断是否做操作

接收方的实现

  • 如果接收到的任期小于当前任期,则立刻返回
  • 如果接收到的chunk offset为0,则开始创建快照文件
  • 在给定的offset处写入数据
  • 如果done为false,返回且等待接下来的数据
  • 保存snapshot文件,丢弃之前存在的所有快照文件
  • 如果现有日志条目的索引和术语与快照的最后一个包含的条目相同,请保留其后的日志条目并回复
  • 丢弃整个日志文件
  • 把状态机重设为快照的内容(也使用快照内的集群信息)

State 状态

所有服务器上的持久化状态

在响应RPC之前更新到持久化存储上

  • 当前任期 server见过的最大任期值(初始值是0,单调递增)
  • votedFor 在当前任期内获得投票的候选人ID(如果没有,则为null)
  • log[] log entry;每一个包含一个状态机的命令,包含从leader获取的任期(初始index为1)

所有服务器上的可变状态

  • commitIndex 已知要提交的最高日志条目的index(初始值是0,单调递增)
  • lastApplied 已知要应用到状态机上的最高index(初始值是0,单调递增)

leader上的可变状态

选举后重新初始化

  • nextIndex[] 对每个服务器,将要送给另一个服务器的下一个log entry的index(初始化为leader的lastLogIndex+1)
  • matchIndex[] 对每个服务器,知道的复制到该服务器的最大的log entry的索引。(初始化为0,单调递增)

AppendEntryies RPC

被leader触发,用来复制log entry;也用于心跳。

参数

  • term leader的任期
  • leaderId 使得follower可重定向client的请求
  • prevLogIndex 紧接新记录之前的日志条目索引
  • prevLogTerm 紧接新记录之前的日志条目索引的任期
  • entries[] 要存储的entries。心跳时为空,批量来提升性能
  • leaderCommit leader的commit index提交索引

返回结果

  • term 当前任期,使得leader可以根据这个结果判断是否做操作
  • success 如果follower包含了匹配的prevLogIndex和prevLogTerm,就返回true

接收方实现

  • 如果term < current term,返回false
  • 如果不包含匹配的prevLogIndex和prevLogTerm,就返回false
  • 如果有一个存在的entry和新的冲突(相同的index,不同的term),删除哪个entry和在其之后的所有entry
  • 添加没在log里面的所有entry
  • 如果leaderCommit > commitIndex, 设置commitIndex为leaderCommit、lastNewEntry的最小值

RequestVote RPC

由candidate调用来收集选票

参数

  • term 候选者的任期
  • candidateId 候选者的请求投票
  • lastLogIndex 候选者的最后log entry的索引
  • lastLogTerm 候选者的最后log entry的任期

返回结果

  • term 任期,可以让candidate根据任期做操作
  • voteGranted true代表候选者收到了选票

接收方实现

  • 如果term < currentTerm返回false
  • 如果votedFor或candidateId是null,并且候选者日志至少比接受者的新,给予自己的投票

Server遵守的规则 Rules for Servers

所有服务器

  • 如果commitIndex > lastApplied: 增加lastApplied,把log[lastAplied]应用到状态机上
  • 如果rpc请求或响应中,包含的任期T比当前的Term值大,则将当前的任期值设置为T,转换自己为follower

Follower

  • 向候选者和leader响应rpc
  • 在选举超时的时间间隔内,没有接收到AppendEntries RPC请求或者投票给其他人,那么切换为候选者

候选者

  • 一旦转化为候选者,开始选举
    • 增加任期号
    • 给自己投票
    • 重置选举定时器
    • 向其他服务器发送RequestVote RPC请求
  • 如果接收到了大部分节点的投票,成为leader
  • 如果AppendEntries RPC从新的leader处返回,变为follower
  • 如果选举超时,启动新的选举流程

Leader

  • 选举完成后:发送空的AppendEntries RPC请求到每个服务器(心跳),在空闲期间重复此操作避免选举超时
  • 从客户端哪里接收到命令;添加entry到本地的日志中,待把这个entry复制到状态机后响应
  • 如果对一个follower有,last log index >= nextIndex,从nextIndex开始发送AppendEntries RPC请求
    • 如果成功:更新follower的nextIndex和matchIndex
    • 如果因为日志不一致的原因失败:降低nextIndex然后重试
  • 如果存在N,且N>commitIndex,大多数节点的matchIndex[i]>=N,并且log[N].term == cuurentTerm,设置commitIndex=N

为什么要采样追踪对接SkyWalking

为了提升Pulsar的可维护性,我们希望能深入pulsarbookkeeper的底层,做性能剖析,识别中间延时高的环节,然后很方便地进行定位分析。

为什么Pulsar原生的普罗监控无法满足

  • 部门基础设施方面,所有环境都集成了ELK,大部分环境都没有普罗,且部门正在引入SkyWalking。
  • 普罗的监控无法和用户的一条消息对应,较难处理单个客户保障。

幸运的是,这两个问题都可以通过采样跟踪来解决。并且在合理的采样跟踪配置下,测试环境可以达成百分百采样追踪,对于我们定位测试环境问题非常方便。

我们就想到了使用经典的采样追踪模式来对数据进行采样,将数据输出到SkyWalking进行下一步的分析、告警。并且在没有对接SkyWalking的环境,通过日志输出来进行下一步的分析、告警

Sample Tracing Basic

image-20210424091405442

  • 通过traceIdspanId结合,识别一条链路。
  • 每个spanId中间计算耗时
  • 为了性能,不会每个spanIdtraceId都收集分析,会进行采样(收集部分消息、收集时延较大的消息)

注:SkyWalking采用STAM来进行拓扑分析,并且引入了Segment等概念来表达进程内、进程外等含义,但大致原理相同。

SkyWalking Architecture

Image

Trace Internal In Pulsar

目前,我们将追踪的头部消息放在BrokerMetadata处,以下为追踪数据流向

message

image-20210506200048088

batch message

image-20210506200035405

Two ways output

Logging

可在ELK上展示,搜索时延大的消息。(注:如搜索4位数的消息)。

SkyWalking

可依赖SkyWalking进行分析,告警。

对比

Logging SkyWalking
采样方式 SkyWalking Way SkyWalking Way
输出格式 Logging SkyWalking protocol through kafka
追踪数据传播方式 protobuf、Broker Metadata protobuf、Broker Metadata

Beyond SkyWalking

在SkyWalking的采样算法之上,支持对时延大的消息再进行采样,输出到日志,进行分析、告警。

采样规则

  • 全局一秒最多xx条,单topic一秒最多xx条
  • 采样时延大于xx的消息

Follow-up

  • 将Pulsar和Bookkeeper的metric信息上报到SkyWalking,使得在SkyWalking分析、告警更多信息。
  • 使用agent方式实现,避免和SkyWalking协议过度耦合

参考

之前就在环境上ps -ef看到过xxxxxx的密码,一直没搞明白怎么回事,今天整理了一下,核心内容均来自于上述连接,作了一些额外的测试和查阅资料。

测试

运行Mysql实例

1
2
# 自己做的Mysql8的镜像
docker run ttbb/mysql:stand-alone

使用密码连接Mysql服务器

1
mysql -u hzj -p Mysql@123 -e "select 1"

ps -ef查看

1
2
3
4
5
6
7
8
9
10
[root@91bcbd15a82e mysql]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 07:34 ? 00:00:00 /usr/local/bin/dumb-init bash -vx /opt/sh/mysql/hzj/scripts/start.sh
root 8 1 0 07:34 ? 00:00:00 bash -vx /opt/sh/mysql/hzj/scripts/start.sh
root 17 1 0 07:34 ? 00:00:00 mysqld --daemonize --user=root
root 62 8 0 07:34 ? 00:00:00 tail -f /dev/null
root 63 0 0 07:34 pts/0 00:00:00 bash
root 98 63 0 07:37 pts/0 00:00:00 mysql -h 127.0.0.1 -u hzj -px xxxxxxx
root 99 0 1 07:37 pts/1 00:00:00 bash
root 122 99 0 07:37 pts/1 00:00:00 ps -ef

Mysql隐藏密码原理

改写了args系统参数,demo如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//
// Created by 张俭 on 2021/4/26.
//
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[]) {
int i = 0;
pid_t mypid = getpid();
if (argc == 1)
return 1;
printf("argc = %d and arguments are:\n", argc);
for (i; i < argc; i++) {
printf("%d = %s\n", i, argv[i]);
}
fflush(stdout);
sleep(30);
printf("Replacing first argument with x:es... Now open another terminal and run: ps p %d\n", (int)mypid);
memset(argv[1], 'x', strlen(argv[1]));
getc(stdin);
return 0;
}

编译并运行

1
2
3
4
5
6
7
gcc password_hide.c
[root@c77dc365cd1a sh]# ./a.out abcd
argc = 2 and arguments are:
0 = ./a.out
1 = abcd
Replacing first argument with x:es... Now open another terminal and run: ps p 55

观测结果,开始看的确有明文密码

1
2
3
4
5
6
[root@c77dc365cd1a sh]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 07:49 pts/0 00:00:00 bash
root 32 0 0 07:51 pts/1 00:00:00 bash
root 64 1 0 07:56 pts/0 00:00:00 ./a.out abcd
root 66 32 0 07:56 pts/1 00:00:00 ps -ef

经过30秒后,已经被复写

1
2
3
[root@c77dc365cd1a sh]# ps p 55
PID TTY STAT TIME COMMAND
55 pts/0 S+ 0:00 ./a.out xxxx

Mysql源码地址

mysql-server/client/mysql.cc line 2054

1
2
3
4
5
6
7
8
9
if (argument) {
char *start = argument;
my_free(opt_password);
opt_password = my_strdup(PSI_NOT_INSTRUMENTED, argument, MYF(MY_FAE));
while (*argument) *argument++ = 'x'; // Destroy argument
if (*start) start[1] = 0;
tty_password = false;
} else
tty_password = true;

PS: 后面,我还在OSX上用go程序尝试修改参数,估摸go程序的args传入是值拷贝,修改完成之后args没有生效,看来这个黑科技只有c程序能使用呀。

需要了解的概念

  • VPC:用户的私有网段
  • peering:多个VPC之间打通的方式,可跨用户

前言

今天微信推送Pulsar社区有个Hackathon比赛, 开始想的idea就是,实现pulsar在华为云上提供服务。因为是社区的比赛,是以一个三方系统的方式在华为云上提供服务,而非是以华为云的名义提供服务。分析了下可行性和能达到的效果,对比了StreamNative的官网上提供的pulsar服务在阿里云托管的能力,能提供的能力差不多,最多只不过是实现了在华为云托管的能力,没有从0到1的突破。

现在,在公有云上买rediskafka这类组件已经变得非常普遍,由公有云供应商提供的中间件往往能给你带来良好的体验,相比三方厂家在云上进行托管,我个人认为云厂商的优势主要在以下三点

网络打通容易

下文说一下不是公有云的供应商能以什么样的方式暴露自己的服务。云厂商可以把中间件的ip地址申请在你的vpc内,对任何应用程序来说,连接都是最方便的。无论是容器化部署、虚拟机部署、和其他vpc peering打通的场景,都可以通信。

低廉的成本

不考虑人力成本,云厂商自运营的价格要低于三方厂家。

监控系统对接

方便地和云厂商的告警、统计系统对接,接收告警通知和报表等。

其中网络打通成本尤为重要,三方厂家好好做监控统计系统,也能给用户较为良好的体验。

三方厂家能提供什么样的Pulsar接入

统一接入

三方厂家自己作为公有云上一个用户,无论这个Region上有多少个租户,都用这一个用户提供服务,这也就意味着无法与每个用户进行私网通信。如果在华为云,利用华为云推出的VPCEP服务(此处应有链接),倒是可以给每个用户提供私网通信,不过这个是做了DNAT地址转换的,跟做了DNAT转换的中间件连接,是非常麻烦的。(懂的自然懂。如果有人想详细了解,可以留言,我可以写一个文章介绍里面的坑)

如果使用公网,又想避免扩容的时候动态申请EIP,动态申请EIP并不复杂,问题是EIP是有配额限制的,这才是关键。那么就需要一个统一的接入点,就需要部署pulsar proxy。到这一步,是每个用户申请一个EIP的,如果还想继续节省EIP,那么可以统一域名接入,后端通过SNI的方式转发,个别流量大的客户,单独把域名指向单独的集群。

pulsar-third-vendor1

Peering打通

Peering打通可以给用户不错的私网体验,需要用户预留一个网段,网段不需要太大,能容纳pulsar所在的vm就行。采用peering打通一般绝不会选择容器化部署,想要两个容器化的集群互通,对网设的要求很高,暂且忽略Service的存在,这要求用户的vpc网段和pod网段和三方厂商的vpc网段和pod网段都不重叠!而且peering打通,给用户私有,再搭建一个k8s集群,对成本影响比较大。主要有如下两个问题

自动化

和客户peering打通,需要较大的权限,如何自动化,最大程度的减少需要的权限。

客户网段和其他网段又做了peering

pulsar-third-vendor2

这个问题其实还好,就是路由规则配置麻烦

总结

Peering打通对用户来说已经比较方便了,相信做到自动化也没有太大的技术难度,只是时间和人力投入的问题。统一接入因为网络打通的原因,不好使用kopmop这些高级特性,此外还有不小的公网带宽成本,羊毛出在羊身上,比较大量的用户也会倾向于Peering打通的模式吧。

前言

自17年入职华为之后,一直在使用配置中心,4年期间经历了自研配置中心到Apollo再到自研配置中心和Apollo并存的场景。总结了一下这几年的配置中心演进流程,想把我们在配置中心上的一些实践分享给大家,实现共同进步。Apollo是一款非常优秀的开源软件,是国人的骄傲。如果对Apollo存在理解错误,还望大家不吝赐教,谢谢。

使用到的配置分类

从场景分类

运维配置,即程序只读的配置

人工配置。通过人工在配置中心界面进行配置,而程序只进行读取,如数据库配置、邮箱服务器配置、网卡配置、子网地址配置等。这部分配置数据不要求代码动态写入。

业务配置,即程序可写的配置

我们是一个SaaS服务,每个用户在上面都有一些业务配置。如用户的证书配置、用户服务器的流控配置等,这些业务配置相对运维配置来说更加复杂,且可能会有唯一性限制,如按用户id唯一。这部分配置数据一般由用户操作触发,代码动态写入,并且通知到各个微服务实例。通常,我们希望这些配置能在界面展示,且支持人为修改。上述逻辑如果由各微服务自己实现,会存在大量重复代码,并且质量无法保证。我们希望由一个公共组件来统一实现这个能力。

从配置是否会有列表可分为单值配置或多值配置

单值配置

整个配置下只是多对key、value。value不是很复杂的格式,往往是整数或字符串。

image-20210330171658154

多值配置

多值配置更加复杂,往往是单值配置在不同的key下,有不同的值。比如下面的配置,用户一和用户二的线程池大小和队列不同

img

第一阶段 自研配置中心

在做云服务之前,我们的配置中心层级数较少。我们以软件的形式交付给客户,软件运行时分为管理面和业务面,配置中心管理着管理面和业务面的配置,最为复杂的场景是多套业务面,这个时候需要保证不同集群、不同微服务下的配置不冲突,配置层级为 集群、微服务、配置。

image-20210324204231586

此时的配置中心是完全自研的,不包含蓝绿、灰度配置这些功能,它独具特色的地方有以下两点:

单配置单表

  • 在存储模型上,每个配置对应一张数据表。
  • 对多值配置比较友好,尤其是复杂业务配置,可以支持各种主键约束。对单值配置,稍微重型了一些。
  • 配置的强Schema限制。这些限制包括类型、大小、长度、是否敏感等限制。这种限制既能为界面修改配置提供良好的体验(如:不同格式不同的输入框、敏感字段,前台输入明文,后台入库加密等),也能在通过接口写入配置时做充分的校验。

通过回调方式来确保配置的可靠

举个例子,添加一个配置的流程是这样的

image-20210324205828998

可能这里,有读者想要问了,这个流程能确保什么可靠呢。这个流程通过调用微服务接口来校验配置是否可靠,如IP地址是否合法、对端地址是否可达、配置数量是否超过规格等等,来保证配置基本可用。

总的来说,这个自研的配置中心在当时综合体验还是不错的。但是也有一些问题有待改进,比如单配置下配置项数量过多时,因为底层有部分接口单配置下所有数据都通过一个http请求来承载,会导致响应超时等问题。

第二阶段 Apollo

开始第二阶段实践的原因主要是,我们进行了组织切换,业务重心转向做云服务,同时团队进行DevOps转型。原先的老配置中心是由另一个团队维护的,组织切换完之后,如果还要使用,就要我们自己维护。所以我们需要在继续维护老配置中心和引入开源Apollo中间进行选择。除了上文中提到的运维配置和业务配置,这个时候我们的需求还有改变:

  • 配置的层级愈发丰富了
  • 要构建灰度发布微服务的能力

老配置中心一方面由于组织切换原因不提供维护了,另一方面不能支撑丰富的配置层级,也不具备灰度发布的能力。这个时候,Apollo的一些特性吸引了我们,这些特性正是老配置中心所缺乏的,例如(部分引用自Apollogithub主页)

  • 丰富的层级,从app_idcluster,namespace,key-value的层级能满足我们region、集群、微服务的层级诉求
  • 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例。
  • 所有的配置发布都有版本概念,从而可以方便的支持配置的回滚。
  • 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。
  • 所有的操作都有审计日志,可以方便的追踪问题。

因此我们选型引入了Apollo,我和我的主管,还有一个其他同事参与了这项工作。我们在Apollo开源代码的基础上做了比较大的改动,主要原因有以下几点

  • 节约成本,将注册中心、数据库替换成我们当前正在使用的组件,因为这两个依赖不是Apollo的核心依赖
  • 继承老配置中心强Schema的优点。
  • 保留回调确认配置的流程,提前拦截错误的配置,降低代码处理异常配置的复杂度
  • 通过spi或环境变量的方式兼容存量老局点使用老配置中心的场景

结合上述原因,我们最终是这么实践的

  • 数据库切换为postgre数据库、注册中心切换到servicecomb

  • 在namespace上实现了Schema,每个namespace都可以注册对应的SchemaSchema要求数据必须是json格式,且json内对应的value必须满足Schema定义的规范(如ip地址、小数、整数等)

    Schema举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
{
"name":"name",
"type":"string"
},
{
"name":"age",
"type":"int",
"max":120
},
{
"name":"ip",
"type":"ipv4"
}
]

那么数据应该是这样的

1
2
3
4
5
{
"name":"hezhangjian",
"age":23,
"ip":"127.0.0.1"
}
  • 在添加或修改配置的时候,实现了回调功能,由回调业务服务确认配置能否添加或修改
  • 配置分层:云服务对应Apolloapp_id,把内部的环境对应到Apollo上的集群,然后将微服务名+配置名拼接成配置名称。

下图展示了业务概念和Apollo概念的对应关系,有些配置是单值配置,有些是多值配置,所以配置项这一层级是可选的。

配置分层示意图

在这段时间的实践中,我们也发现如下问题

并发问题

其中最致命的就是并发问题,首先Apollo所有配置都存在一张表中,其次由于Apollo设计之初主要考虑的是运维人员手动在界面上操作,代码无并发语义(或者说没给客户端并发语义),使得我们通过代码写入配置时难以解决并发问题。

性能问题

打开namespace列表页面,需要显示这个app_id下的所有namespace,因为我们单app_id会存放单个云服务的所有配置,这个量很大,且界面不支持分页,导致页面加载缓慢

体验问题

Apollonamespace界面未提供搜索功能(可能Apollo设计之初也没想支持这么多),想要从namespace中定位到我们想要查看或修改的namespace,只能借助浏览器的搜索能力。

第三阶段 Apollo与自研配置中心并存

除了上述几个问题,还有一些原因使得我们开始了第三阶段的实践

  • 原来自上而下的配置分层模型,微服务间配置没隔离,不仅不易进行权限管理,而且不适合DevOps 单微服务自治的发布理念。
  • 第二阶段对Apollo改动太多,组织结构变动,没有足够的人力维护
  • 随着集群越来越多,回调功能需要网络的双向打通,网络维护不太方便
  • 我们对Apollo界面以及接口基于业务做的改动较多,导致其他兄弟部门难以共用Apollo

当时大家对是否保留Schema回调检查代码写配置这三个功能点有较大的争议。我个人最希望保留Schema回调检查,因为它们优点显著,而且接口是兼容的,可以与其他部门共用,但是增加了Schema这个概念和回调检查这个流程,会增加学习成本。而代码写配置,由于要解决并发问题,代码改动量较大,我不建议保留。

大家经过激烈的讨论,最终还是废弃了Schema回调检查代码写配置这三个功能点,仅仅把运维配置放在Apollo

然后,我们把业务配置,放在了一个自研的强Schema的配置中心上,这个配置中心,仅负责单集群的配置,每个集群部署一套,满足了我们的业务需求。自研强Schema配置中心的核心要点有,单配置单表、通过注册中心回调来检测配置是否合法、借助mqtt协议来实现长链接推送,无单点瓶颈。

而我们的运维配置中心Apollo回归到了开源的版本,重整了配置的结构,

image-20210405224010878

对运维配置而言好处有

  • 配置模型适合单微服务发布
  • 配置按微服务组织,一个页面上的namespace不会很多

缺点

  • Schema缺失后,不会对操作人员在界面的配置进行校验,即使配置格式或者内容错误也能配置成功。界面上配置密码不支持明文(Apollo无法感知是否为敏感字段),必须提前使用其他工具将明文转换为密文,然后再进行配置。
  • 回调检查功能去掉后,有些配置,如网卡网段配错,操作人员不能即时得到响应

最佳实践

业务配置经过我们的实践,确实不适合使用开源的Apollo。运维配置使用原生的Apollo,但是现在还不具备回调检查Schema的功能,希望Apollo能在后续版本中支持Schema,或者弱化的json格式检查功能。下面是我们在如下场景下的最佳实践

SRE在界面上的运维配置

通过Apollo来实现功能,至于配置如何组织,根据大家的组织结构、技术架构来对应Apollo上的概念,可按照微服务->部署环境部署环境 -> 微服务的层级来组织配置

复杂的参数校验

建议在Apollo上面自建portal包裹一层,后端服务可先进行一层处理,这一层处理可以做比较复杂的格式化校验甚至回调检查,再调用Apollo OpenApi将配置写入Apollo

业务配置的技术选型

最大的挑战是业务配置由用户触发,请求的并发不易处理。思路有两个,一个是在Apollo原生代码的基础上,通过数据库分布式锁来解决并发问题。第二个是借鉴我们的思路,通过单配置单表、mqtt协议实现通知等核心技术点,自研业务配置中心。

业务配置的部署

需要根据业务配置的数量来考虑是否合设业务配置中心。单集群场景下,毫无疑问只需要一个业务配置中心,甚至如果使用Apollo实现,可以考虑和运维配置中心合设。多集群场景下,部署一个业务配置中心,还是多个业务配置中心,我们自己的实践中,一个集群往往要支撑数万用户,我们采取了每个业务集群部署一套业务配置中心的策略。

Kubernetes pod内调用API的流程总体分为以下步骤

  • 创建role
  • 创建serviceaccount
  • 绑定role到serviceaccount
  • 指定pod使用serviceaccount

我们以查pod为例,演示一下整个流程

创建role

1
2
3
4
5
6
7
8
9
10
# role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: role-hzj
namespace: default
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list"]
1
kubectl apply -f role.yaml

创建serviceaccount

1
2
3
4
5
6
# serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: serviceaccount-hzj
namespace: default
1
kubectl apply -f serviceaccount.yaml

绑定role

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: rolebinding-hzj
namespace: default
subjects:
- kind: ServiceAccount
name: serviceaccount-hzj
namespace: default
roleRef:
kind: Role
name: role-hzj
apiGroup: rbac.authorization.k8s.io
1
kubectl apply -f rolebinding.yaml

部署pod进行测试

部署一个zookeeper进行测试

手上刚好有zookeeper的模板文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: apps/v1
kind: Deployment
metadata:
name: zookeeper
labels:
app: zookeeper
spec:
replicas: 1
selector:
matchLabels:
app: zookeeper
template:
metadata:
labels:
app: zookeeper
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: zookeeper
image: ttbb/zookeeper:stand-alone
imagePullPolicy: IfNotPresent
resources:
limits:
memory: 2G
cpu: 1000m
requests:
memory: 2G
cpu: 1000m
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: PS1
value: '[\u@zookeeper@\W]\$ '

调用API

1
2
3
4
5
6
7
8
9
10
11
12
13
# Point to the internal API server hostname
APISERVER=https://kubernetes.default.svc
# Path to ServiceAccount token
SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
# Read this Pod's namespace
NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)
# Read the ServiceAccount bearer token
TOKEN=$(cat ${SERVICEACCOUNT}/token)
# Reference the internal certificate authority (CA)
CACERT=${SERVICEACCOUNT}/ca.crt
# Explore the API with TOKEN
curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api
curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api/v1/namespaces/default/pods

kubernetes-pod-api1

发现这里,调用后面的api,403错误。第一个api不报错,是因为该接口不需要鉴权。

修改pod对应的serviceaccount

让我们修改部署模板对应的ServiceAccountName,注入权限。在pod的spec下,设置serviceAccountName

kubernetes-pod-api2

修改部署模板重启后调用api正常

再次尝试上述命令,api结果返回正常

kubernetes-pod-api3

0%