最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

在线支付公司 Stripe 的服务发现架构设计过程分享

  • 2016-12-20
  • 本文字数:5433 字

    阅读完需:约 18 分钟

本文翻译自 Service discovery at Stripe ,原作者为 Julia Evans,翻译已获得原网站授权。

每一年,各种新技术都会如雨后春笋般不断地冒出来,就像 Kubernetes Habitat 那样,所以在兴奋之余我们常常会在内心中感到不安,因为我们一直默不作声地用各种技术来支撑着我们的生产系统,却没有向它们表示敬意。在我们 Stripe 有一个这样的用了好几年的工具就是 Consul 。Consul 帮我们实现了服务发现功能(也就是说,在我们的几千台服务器上运行了各种各样的服务,它帮我们路由,并且告诉我们哪些服务在运行着,并且可用)。这种实用有效的架构选择并非一时的心血来潮或赶时髦,它非常忠实地完成了我们的使命,帮我们为遍布世界各地的用户提供了可靠的服务。

接下来我们会谈到:

  • 服务发现和 Consul 是什么;
  • 我们是怎样管理部署关键软件的风险的;
  • 我们常常遇到的挑战,以及我们是怎样应对的;

你并不会只是把新的软件搭起来,然后就希望它可以像变魔术一样开始工作,并且解决掉你的所有问题——使用新软件是一个过程。下面的例子就表明了在生产环境中,使用新软件的过程对我们来说像怎么一回事。

(点击放大图像)

什么是服务发现?

好问题!假设你是 Stripe 的一个负载均衡器,现在来了一个请求要创建一个帐单记录。你想把它发往一台 API 服务器,任何一台 API 服务器都有可能!

我们运行了几千台服务器,在其之上运行了各种各样的服务。哪些是 API 服务器呢?那些 API 运行在哪些端口上呢?使用 AWS 的一个非常令人吃惊的特点就是,我们的实例可能在任何时候宕机,所以我们必须要准备好应对下面这些情况:

  • API 服务器任何时候都可能会丢失不见了;
  • 在需要更大的容量时,就添加更多的服务器;

这种不断检查地各台服务器的可用状态变化的过程就叫服务发现。我们用了 HashCorp 的一个名为 Consul 的工具来做服务发现。

我们的实例可能在任何时候宕机这件事,事实上是非常有用的——从实际运行情况来看,我们的基础设施中经常会有实例宕机,而我们也能自动处理,所以当发生这样的事时,对我们来说不过是非常正常的例行公事。如果经常发生故障,那就更容易非常优雅地处理故障。

介绍 Consul

Consul 是一个服务发现工具:它让服务自己过来注册,并且可以发现其它的服务。它把那些可用的服务保存在数据库中,并且有客户端软件可以将这样的信息保存在那个数据库中,这样其它的客户端软件就可以从数据库中读取信息了。在这里还有很多细节需要仔细考虑。

对 Consul 来说最重要的模块就是数据库。数据库里的记录是像这样的:“API 服务运行在 IP 10.99.99.99 的 12345 端口上。它现在正在运行。”

每台服务器都会向 Consul 发布信息,内容类似这样:“嗨!我这里的 12345 端口运行着 API 服务!现在的状态是运行中!”

然后如果你需要和 API 服务进行交互,你就可以直接问 Consul:“哪里有 API 服务可用?”它就会给你返回一系列的 IP 地址和端口,然后你就可以访问了。

Consul 本身是一套分布式系统(记住:我们有可能在任意时刻宕掉某台服务器,这就意味着我们的 Consul 服务器自身也可能会宕掉),所以它用了名为 Raft 的一致性算法来保证数据库中的数据是同步的。

如果你对 Consul 内部的一致性问题感兴趣,可以在这里读到更进一步的内容。

在Stripe 刚开始用Consul 的时候

我们最初只向Consul 写数据——让各台服务器自己向Consul 服务器报告它们是不是在正常运行,但并没有用那些信息去做服务发现。我们写了一些 Puppet 配置来把它搭起来,这事并不困难。

这样我们就可以通过运行 Consul 客户端来发现潜在的问题,并且得到在几千台服务器上运行它的经验了。一开始,用 Consul 压根发现不了服务。

是哪里出问题了呢?

解决内存泄露问题

如果你往你的基础设施中的每一台服务器上都增加一个新软件,那么那个软件肯定会出问题!最早的时候我们就发现了 Consul 的统计库里的内存泄露问题:我们发现有一台服务器用了超过 100MB 的内存,并且数字还在不断变大。那是 Consul 的一个漏洞,后来被修复了。

100MB 的内存泄露得并不算多,但是泄露的量却增长得很快。内存泄露问题通常都是令人担忧的,因为这是让一个进程把一台服务器的运行环境完全破坏掉的最容易的办法,这样别的进程就都无法运行了。

好在最开始的时候我们没有用 Consul 来做服务发现。我们只把它部署在了一部分生产服务器上,并且在监控着内存使用量,所以我们可以很快地发现潜在的严重问题,没有产生什么负面影响。

开始用 Consul 发现服务

当我们很有信心 Consul 运行在我们的基础设施中可以工作得很好的时候,我们就开始让一些客户端与 Consul 交互了!为了降低风险,我们做了这两方面的努力:

  • 开始时只把 Consul 用在少数几个地方;
  • 时刻保留着一套后备系统,这样万一出现故障我们还可以继续提供服务;

我们碰到了下面的几个问题。我们把它们列在这里并不是在抱怨 Consul 做得不够好,而是在强调当你使用一项新技术时,最重要的事就是要慢慢地扩大规模,一定要非常小心。

非常多的 Raft 故障切换:还记得 Consul 使用了一致性协议吗?它用一致性协议来把 Consul 集群中一台服务器上的所有数据复制到其它服务器上去。主服务器的磁盘 IO 能力非常有问题,磁盘不够快,没办法像 Consul 希望的那样把数据读出来,这样整台主服务器都会失去响应。然后 Raft 就会认为:“啊,主服务器宕机了!”然后再去选举出一个新主,这样的事就一遍一遍地发生。当 Consul 忙于选举新主时,它会拒绝掉所有对数据库的读或写请求(因为一致性读是默认要求)。

0.3 版的 SSL 功能是有问题的:为了让 Consul 节点之间可以安全地通信,我们启用了 Consul 的 SSL 功能(技术上叫 TLS)。有一个 Consul 的发行版这个功能是有问题的。我们打了补丁。这个例子说明有一类问题是很容易发现,并且不用害怕的(我们在QA 测试中发现SSL 出了问题,于是没有部署这个版本),但这样的事情对于早期阶段的软件也是很寻常的。

Goroutine 泄露:我们开始时用了 Consul 的主选举功能,可是有个Goroutine 泄露问题,使得Consul 会快速消耗掉服务器上的所有内存。Consul 团队在处理这个问题时帮了很大的忙,我们解决了许多内存泄露问题(和以前的内存泄露不同)。

当所有这些问题都解决掉之后,我们就处于非常有利的位置了。从“第一次使用Consul 客户端”到“我们已经解决了生产环境里的所有这些问题”,这大概花了我们不到一年的时间。

将Consul 大规模部署,用于发现哪些服务是可用的

现在我们已经了解了Consul 的一些漏洞,并且修复了,那所有东西就都可以运作得更好了。还记得我们在最开始时说到的步骤吗?你问Consul:“喂,有哪些服务器是提供API 服务的?”当时我们总是断断续续地碰上一些问题,Consul 服务器有时候会响应慢,有时候压根没响应。

在出现Raft 故障切换或者不稳定时,这种情况就非常常见,因为Consul 用的是一个强一致性的数据库,所以相比于一致性弱一些的数据库,它的可用性就相对差一些。在早期,这个问题非常突出。

我们还有后备方案,但Consul 停止服务总会给我们造成很大麻烦。当Consul 服务出故障后,我们就不得不回退,使用一些固定的DNS 名字。在最初推广Consul 时这种方案还可以,但随着规模地扩大和Consul 的使用日益广泛,这种方案就变得越来越不灵活。

用Consul Template 来救急

通过Consul 的HTTP API 来向Consul 询问哪些服务是可用的,这样做并不可靠。但不管怎样,有它我们还是很高兴。

我们希望可以不用API 就从Consul 之中查出服务的可用信息,该怎么做呢?

Consul 主要就是输入一个名字(比如叫 monkey-srv),然后把它翻译成一个或多个 IP 地址(“这里有 monkey-srv”)。还记得有什么别的东西也是输入名字,然后输出 IP 地址的吗?DNS 服务器!于是我们就用一台 DNS 服务器来替换了 Consul。具体是这么做的:用 Consul Template,这是一个 Go 程序,可以根据你的 Consul 数据库里的内容来生成静态配置文件。

于是我们就开始用 Consul Template 来为 Consul 服务生成 DNS 记录。比如如果 moneky-srv 运行在 IP 10.99.99.99 上,我们就会生成这样一条 DNS 记录:

monkey-srv.service.consul IN A 10.99.99.99

代码中看起来像是这样。你也可以看看我们的真正的 Consul Template 配置,那个会更复杂一些。

复制代码
{{range service $service.Name}}
{{$service.Name}}.service.consul. IN A {{.Address}}
{{end}}

可能你在想:“等等!DNS 记录中只有 IP 地址,你还需要知道这个服务在侦听着这台服务器的哪个端口。”是的,你说对了。DNS A 记录是大家最常见到的一种,里面只有 IP 地址。但是,DNS SRV 记录中就包含了端口信息,我们正是用 Consul Template 来生成 SRV 记录的。

我们为 Consul Template 配置了定时任务,每 60 秒钟运行一次。Consul Template 的默认模式是“观察”模式,每当数据库中的内容有更新时,它就会不断地更新配置文件。可是当我们试用观察模式时,它的运行效果就像是对我们的 Consul 服务器发起了 DOS 攻击,于是我们就不再用它了。

所以如果我们的 Consul 服务器宕机了,那我们的内部 DNS 服务器仍然是有着全部记录的。可能数据会有些旧,但关系不大。更棒的是我们的 DNS 服务器并不是什么精巧的分布式系统,也就是说它只是一个简单的程序,所以不大可能会自己出故障。那么,我就可以简单地直接查询 monkey-srv.service.consul 来获得一个 IP,然后再用它去直接与我的服务交互了。

因为 DNS 是一种无共享最终一致性系统,所以我们可以复制,并一批一起做缓存。我们有五台主要的 DNS 服务器,每台服务器都有一个本地的 DNS 缓存,并且缓存知道该如何与这五台 DNS 服务器中的任意一台交互。所以它的确会比 Consul 更有弹性。

为了更快的健康检测而增加一个负载均衡器

我们刚提到了每 60 秒钟会用 Consul 的数据更新一次 DNS 记录。那么,万一某台 API 服务器宕机了怎么办?我们会不断地向那个 IP 发送请求,直到 45 秒或更长的时间之后,DNS 服务器中的数据更新了才停止吗?不会的!这里就涉及到了另一项技术: HAProxy

HAProxy 是一个负载均衡器。如果你对请求发向的服务做健康检测,那就可以确保你的后端是可以提供服务的!事实上我们所有的 API 请求都会经过 HAProxy。它具体是这么工作的:

  • 每 60 秒钟,Consul Template 写一个 HAProxy 配置文件。
  • 这意味着 HAProxy 对后端的状态数据总是大概正确的。
  • 如果某台服务器宕机了,那 HAProxy 马上会就发现有些东西出问题了(因为它每 2 秒做一次健康检测)。

这意味着我们每 60 秒钟会重启 HAProxy 一次。那我们重启 HAProxy 时会把所有现有连接都断掉吗?不会的。为了避免因重启而断掉连接,我们使用了 HAProxy 的优雅重启功能。当然重启的过程中处理量会有所下降(在这里有所描述),但我们不觉得这是个大问题。

我们的每个服务都有标准的健康检测节点——差不多每个服务都有个/healthcheck 节点,正常情况下会返回200,否则就表示出错。标准化很重要,因为这样我们就可以轻松地配置HAProxy 来检测服务的健康状态了。

假如Consul 不能提供服务了,HAProxy 就会一直使用一份过期的配置文件,这样也能继续提供服务。

牺牲一致性,换取可用性

如果你观察得足够仔细,你会发现我们最初用的系统(一个强一致的数据库,可以保证里面存储着最新的状态)与我们最终的系统(一台DNS 服务器,数据可能会有一分钟的延迟)相比,两者有很大的不同。我们放弃了对一致性的追求,因此获得了一套可用性更高的系统:Consul 的故障基本上不会对我们的服务发现功能造成任何影响。

从这里可以学到的很重要的一点就是,一致性不是那么容易达到的!你将不得不付出可用性的代价,而且如果你决定使用一套强一致性的系统,那请一定保证那的确是你需要的东西。

当你发出一个请求时会发生什么

我们在这篇博客中包含了许多内容,所以现在让我们跟踪一遍请求的处理流程,看看我们刚才了解的东西都是怎么工作的。

当你请求 https://stripe.com/ 的时候,会发生什么?它最终会不会被正确的服务器处理?下面是一点简单的解释。

  1. 请求到达一台对外开放的公共负载均衡器,上面运行着 HAProxy;
  2. Consul Template 已经将所有可以处理 stripe.com 的服务器生成了一个列表,保存在配置文件 /etc/haproxy.conf 中;
  3. HAProxy 每 60 秒钟重新读一次配置文件的内容;
  4. HAProxy 把你的请求发送到一台 stripe.com 服务器上!它知道这台服务器是在提供服务的。

事实上的流程是比上面描述的更复杂一些的,因为本来还会多一层中间层,而且 Stripe 的 API 请求也更复杂,因为我们还有系统要处理 PCI 合规性问题。但所有的核心内容都在上面了。

这意味着当我们启动或关闭服务器时,Consul 会自动处理好相应的记录,完全不需要手工介入。

一年多的平静

在我们服务发现功能的实现中,还有很多地方是我们想要改进的。这是一片开发活动非常活跃的天地,我们已经看到在不久的将来,我们可以非常优雅地将调度和请求路由系统整合起来。

不过,我们得到的最大收获之一,就是简单的方法往往是最有效的。这套系统已经非常可靠地为我们服务了超过一年了,没有出过任何差错。Stripe 的请求处理量远不及 Twitter 或 Facebook,但我们还是对可靠性有着非常极致的追求。有时候最大的成功就是部署了一套稳定的、优秀的解决方案,而不是什么新奇的东西。

对 Stripe 公司有兴趣的朋友可以在这里了解技术团队的需求


感谢郭蕾对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016-12-20 16:493292
用户头像

发布了 152 篇内容, 共 68.0 次阅读, 收获喜欢 63 次。

关注

评论

发布
暂无评论
发现更多内容

HTTP系列之:HTTP中的cookies

程序那些事

Java 网络协议 HTTP cookies

(深入篇)漫游语音识别技术—带你走进语音识别技术的世界

声网

深度学习 音视频 语音识别

Vue进阶(九十):过滤器

No Silver Bullet

Vue 9月日更

【LeetCode】 二叉树中和为某一值的路径Java题解

Albert

算法 LeetCode 9月日更

架构学习模块一

George

如何设计企业特色的数字化转型架构?

博文视点Broadview

多线程知识体系01-线程池源码阅读讲解-Executor

小马哥

多线程 高并发 源码阅读 源码剖析 日更

华为云PB级数据库GaussDB(for Redis)揭秘:如何搞定推荐系统存储难题

华为云开发者联盟

数据库 推荐系统 存储 华为云 GaussDB(for Redis)

LeetCode刷题278-简单-第一个错误版本

ベ布小禅

9月日更

Go 专栏|变量和常量的声明与赋值

AlwaysBeta

Go 语言

Go 专栏|流程控制,一网打尽

AlwaysBeta

Go 语言

【Flutter 专题】58 图解 Flutter 嵌入原生 AndroidView 小尝试

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 9月日更

Go 专栏|基础数据类型:整数、浮点数、复数、布尔值和字符串

AlwaysBeta

Go 语言

Go 专栏|复合数据类型:字典 map 和 结构体 struct

AlwaysBeta

Go 语言

Go 专栏|说说方法

AlwaysBeta

Go 语言

模块(二)如何设计架构

我是一只小小鸟

Linux内核四大核心框架

hanaper

Go 专栏|函数那些事

AlwaysBeta

Go 语言

Go 专栏|接口 interface

AlwaysBeta

Go 语言

Electron团队为什么要干掉remote模块

刘晓伦

Electron Node

升级mysql-connector-java-8.x踩坑纪实

小江

Java MySQL 时间戳 服务器时区 夏令时

Go 专栏|复合数据类型:数组和切片 slice

AlwaysBeta

Go 语言

Go 专栏|错误处理:defer,panic 和 recover

AlwaysBeta

Go 语言

Linux之lastlog命令

入门小站

Linux

【Vue2.x 源码学习】第四十三篇 - 组件部分 - 组件相关流程总结

Brave

源码 vue2 9月日更

线程同步类CyclicBarrier在性能测试集合点应用

FunTester

多线程 性能测试 线程安全 测试框架 FunTester

【报名】飞桨中国行丨企业零门槛AI创新应用-智能制造专场

百度大脑

人工智能

在线JSON转JAVA工具

入门小站

工具

柯基数据通过Rainbond完成云原生改造,实现离线持续交付客户

北京好雨科技有限公司

云原生 需求落地 离线部署 可持续交付

就靠这一篇文章,我就弄懂了 Python Django 的 django-admin 命令行工具集

梦想橡皮擦

9月日更

从一个并发异常问题引起的想法

卢卡多多

并发编程 9月日更

在线支付公司Stripe的服务发现架构设计过程分享_架构_Julia Evans_InfoQ精选文章