@xuemingdeng
2017-03-18T14:25:19.000000Z
字数 15910
阅读 703
正文:
2016年底,Evernote对他们的服务进行了一次大规模的迁移。他们将Evernote服务从自有数据中心迁移到了Google Cloud Platform上。他们在短短的70天内完成了整个迁移过程,包括Evernote服务、存储数据和其他配套服务。参与这次迁移的工程师们在Evernote官方博客上分享了这次迁移的过程。文章包含了5个部分。以下译文已获得翻译授权。
Evernote服务在2008年开始投入使用,我们一直维护着自己的服务器和网络。我们有很大的自由度来构建服务,但同时也面临着一些挑战:伸缩困难、升级缓慢、高昂的维护成本。虽然我们的基础设施在过去为Evernote提供了很好的支持,但在未来,我们需要更快的速度和更大的灵活性,而这些正是目前的基础设施所缺乏的。
迁移到公有云这一决定让Evernote的每一位员工都兴奋不已。在首次发布迁移声明之后,我们便在幕后启动了从物理数据中心到Google云平台(GCP)的迁移工作。经过70天日以继夜的奋战,我们完成了迁移。
可能有些人想了解我们的迁移过程,又或者想进行类似的迁移,所以我们决定把迁移过程分享出来。我们会告诉大家在迁移过程中我们都做了哪些事情,以及如何能够快速地完成迁移。这不是一个完整的迁移手册,不过它涵盖了我们做出的所有关键决策,以及我们是如何实现它们的。
先让我们来了解一下Evernote服务的概况。
我们的服务由以下几个组件组成。
分片(NoteStore)
分片是Evernote服务的核心单元,用于存储用户的笔记。每个分片最多可以支撑30万个Evernote用户,并包含了如下几个组件。
- 基于Tomcat的前端Web服务层:Evernote客户端会连接到这个层。
- 数据存储层:用于存储用户笔记的MySQL数据库。
- 搜索索引:基于Lucene的服务端搜索索引,用于搜索用户的笔记内容。
我们总共有762个分片,存储着2亿个用户账号和大约50亿个用户笔记。
UserStore
UserStore使用MySQL数据库来存储用户信息,包括他们的认证信息。因为数据库管理着所有用户的状态和认证信息,所以它是最为关键也是最为复杂的一个组件,在迁移过程中我们需要格外小心。
用户附件存储(资源)
我们有一个单独的文件存储层,用于存储50亿个用户附件(我们把这些附件称为资源)。这个层由206个自包含的WebDav服务器组成。每个用户附件有三个副本,其中两个分别被保存到本地的两个不同的WebDev服务器上,另外一个被发送到远程灾备数据中心的WebDav服务器上。
前端负载均衡
我们的高可用负载均衡器集群负责将用户请求路由到特定的分片上。
配套服务
我们还有200多台Linux服务器用于缓存和执行批处理任务,比如笔迹识别和文本识别。
Evernote服务规模庞大,向云端迁移是一项复杂的工程,我们需要作出很多与依赖项有关的决策。我们希望能够尽快完成迁移,所以我们为关键性决策制定了一个草案。
我们要先理清楚需要做出哪些变更。我们知道,有些组件无法直接被简单地迁移到云端,所以我们把组件分为两类。
- 转移:有些组件在CGP上能够找到几乎一样的替代品,分片、UserStore以及大部分配套服务都属于这一类。在我们的物理数据中心,这些组件都是基于Linux的,所以我们将会直接把它们迁移到云端的Linux虚拟机上。
- 转换:在迁移过程中需要对用户附件存储、负载均衡层和识别服务(Reco)做一些重大的转换。对于这些组件来说,要么在云端找不到相应的替代方案,要么云端已经存在更好的替代方案。
接下来我们要开始计划如何进行迁移,我们有两种选择。
- 一次性迁移:迁移项目里存在着一个分界点,跨过这个分界点就等于完成了100%的切换。对于自有数据中心和采用了单体架构的组织来说,这种方式会更适合(这或许也是唯一可行的方案)。
- 阶段性迁移:这是一种分而治之的方法,根据类型或用户将服务分组,进行阶段性的迁移。采用这种方式可以在提交阶段性工作之前对其进行测试和验证。
从我们的情况来看,一次性迁移的方案并不适合我们。即使做了详尽的计划,一次性迁移完所有的组件仍然存在很大的风险。况且,我们的应用并非为多站点运行而设计的,尽管我们的环境在很早之前就已经支持多站点运行。
所以,我们需要在上述的两种极端之间找到一个折衷的方案,我们决定采取“增速阶段性切换”的方案。整个迁移过程要在短短的20天内完成,它包含了多个阶段,每个阶段的风险需要被降到最低。如果某些阶段的工作无法达到预期的效果,可以进行回滚。
我们的安全团队负责保护用户数据的安全。在向云端迁移的过程中,我们要考虑如何保证用户数据的安全。我们面临两个主要的问题。
- 我们即将把数据交由Google来看管,他们是否拥有足够成熟的安控措施,能够避免给我们的服务带来新的风险?
- GCP是否提供了等价的或者更好的安控措施来保证数据的安全?
我们内部有一个厂商审查流程,由我们的法律团队和安全团队共同执行这个流程。我们在与新的服务提供商展开合作之前,会对相关的隐私和安全问题进行审查。
在厂商审查过程中,我们会检查60多项与隐私和安全有关的问题。这些内容与我们内部程序的结构保持一致,通过这些审查,我们就可以知道厂商是否符合我们的预期。审查内容可以被分为几类。
- 组织层面的控制措施
- 基础设施的安全
- 产品安全
- 物理设施安全
- 与隐私有关的数据使用
我们和Google一起对他们的审计报告进行了评审,发现他们在各个方面都能够满足我们的预期。
接下来,我们将注意力放在他们是否提供了用于保护用户数据安全的安控措施上。
首先,我们对现有的所有用于保护用户数据的安全措施进行了评审。这些安控措施包括使用了双因子认证的远程访问VPN和提供了流量过滤功能的防火墙。还有其他一些物理安控措施,比如物理外围、生物技术认证、探头,以及用于防止物理数据被盗的警报系统。我们在安全主页上有很多关于基础设施安全防护的讨论。
我们列出了一个表格,记录了迁移过程中需要注意的安全问题,下面是一些主要内容。
- 外围网络安全(向内流量和向外流量过滤)
- 内部隔离
- 多因子认证
- 传输加密
- 缺陷管理
- 身份信息和访问控制
- 诊断日志
- 入侵检测
- 变更监控
我们还需要考虑多租户云环境可能会出现新的威胁模型。内存和存储的使用方式与过去也不一样。我们还要考虑来自其他用户的威胁,这些用户与我们处在同一虚拟层上。好在Google已经考虑到了这些问题,他们在博客上就如何解决这些问题进行过讨论。
我们在GCP上为我们的大部分安控措施找到了等价的替代方案。我们还为静态数据加密找到了一种更好的替代方案。而对于其他一些安控措施,比如IP白名单,我们只能通过改造我们的架构,让它不再依赖传统的网络安控。我们使用包含了Google托管密钥的CGP服务账号来实现这一改造。
迁移到云平台之后,原先的静态CIDR块被静态公共IP和动态公共IP的混合体所替代。维护IP白名单的成本非常高,而且在CGP的其他服务里并不存在这样的功能。
在我们的安控架构里,网络和用户数据之间包含至少两个安全层。内部服务全部被部署在一个安全的网络外围里,服务之间通过API密钥进行交互。我们以一种安全的方式来存储和分发这些密钥,不过仍然存在泄露或被盗的风险。如果真的发生泄露或被盗,我们还有第二个安全层。在我们的生产环境之外,秘钥失去了它们的作用,而要想进入生产环境使用这些秘钥,必须先通过双因子认证。
每一个GCP服务都是互联网服务,它们并没有提供面向用户的白名单安控机制用于访问Google计算引擎(GCE)项目里的主机。我们需要在密钥和用户数据之间添加另一个安全层。
我们使用GCP服务账号解决了这个问题。每个GCE项目都有一个默认的服务账号,在GCE中启动的实例可以通过这个账号来访问其他服务。Google提供了一组公钥/私钥对,密钥对每24小时自动翻转一次。对于自定义的服务账号,也会执行相同的操作。我们可以为每个主机角色创建一个自定义服务账号,并通过配置虚拟实例来使用这些账号。现在,那些运行在这些虚拟实例上的应用就可以使用内置的Google托管密钥。
我们的运维工程师没有必要访问这些密钥,因为Google每天都会自动翻转这些密钥。要想访问到这些密钥只能通过渗透我们的基础设施,不过我们目前已经具备了相应的防护能力。
总的来说,我们对云平台的安全充满了信心。我们还会继续加强平台的安全,把不断变化的安全威胁甩在身后。
下一个重大决定是如何在即将实施的系统架构方案上达成一致。下面是一些重要的考虑点。
- Evernote服务可以运行在多个数据中心上(不包括中国地区的服务)。
- 将用户数据的两个副本保存在不同的地理区域。
- 最小化加州北部数据中心和GCP之间的网络延迟,为迁移工作带来更多的灵活性。而且如果有可能,在短期内还能支持多站点运行。
基于这些考虑,我们达成了以下方案。
- 主要位置(处理生产流量):US-West1
- 次要位置(灾备站点):US-Central1
后续我们还会把US-West1的服务拆分成两个区域,进一步提升故障防护能力。
正如Google所说的,“Google把区域设计成相互独立的:每个区域有自己的供电系统、冷却系统、网络和控制面板,大部分的故障只会影响到单个区域”。
基于上述的架构,我们可以处理US-West1之外的流量,并在US-Central1保存用户数据的第二个副本。如果US-West1发生故障,我们可以使用US-Central1的数据进行恢复。
我们以后还要想办法对我们的应用进行重构,提升它们的弹性。还要考虑如何同时处理多个区域的流量,以便进一步减少从故障中恢复的时间。我们也会考虑如何更好地利用GCP的全球基础设施来改善用户的延迟体验。
到现在为止,我们定义了清晰的需求,并做出了很多决策。接下来开始进入具体的实现阶段。
最大化从数据中心到GCP的网络连接
我们在一开始就意识到,从数据中心到GCP的网络连接是决定这次迁移成功与否的关键因素,同时也是主要的约束所在。为了能够灵活地对数据和服务进行迁移,网络互连计划需要实现如下几个目标。
- 对从数据中心流向CGP的流量进行加密。
- 将两个区域中的任意一个作为用户流量的“前门”,如果有必要,可以基于这两个“前门”来分割流量。
- 对每个区域的服务进行分割,让它们部分运行在物理数据中心里,部分运行在GCP上。
- 最大化站点间的带宽,以支持大块数据的拷贝。
在GCP上运行Evernote后端服务,并向CGP拷贝3PB的数据。在这种情况下,为了让100%的“前门”流量流经数据中心和物理负载均衡器系统,我们需要进一步提升灵活性。
我们已有的外部网络连接可以处理峰值负载,而且留有余量。不过,它的容量仍然无法满足及时清退数据的要求。
另外,我们的内部网络结构并不支持将如此大规模的请求导出到外部设备(比如Google云存储)上。基于目前的情况,要将所有的数据上传到云端,可能需要超过一年的时间,而且会影响用户体验。所以我们还有很多工作要做。
首先,我们需要建立私有网络互连(PNI),或者直接在Evernote的网络和GCP之间建立连接。这样可以将可用带宽增加一倍,而且这些连接独立于用户流量。这就在Evernote和GCP之间建立起了一个快速的私有通道。
其次,我们需要确定数据中心的哪些地方需要输出数据。我们的数据有多个副本,所以需要确定该使用哪个副本。我们需要开辟出一条网路,在不影响系统正常运行的情况下,将成千上百台服务器的数据通过私有通道移动到GCP。我们需要小心地协调数据拷贝作业,让这些请求正确地流经一系列负载均衡器和代理服务器。
在项目启动后的头一个月,我们的网络工程团队争取在数据拷贝启动之前完成准备工作。因为如果他们的工作无法按时交付,整个迁移工程都会受到影响。
我们可以多站点运行吗?
到目前为止,我们的应用只在单个数据中心里运行。在单个数据中心里,节点间的延迟通常是亚毫秒级的。如果我们能够成功地在数据中心和GCP上运行应用,我们需要知道该如何将节点间的延迟保持在20毫秒到50毫秒之间。延迟的增加有两方面的原因,一方面是受光缆的速度限制,另一方面是受数据中心和GCP之间距离的影响。
很显然,我们不希望在迁移过程中发生此类问题。为了避免给用户带来延迟,我们需要先进行自测。在项目计划期间,我们决定使用一种服务端工具(tc)引入人为的网络延迟,并模拟出因地域和光缆速度限制所带来的延迟。我们逐渐将NoteStore的延迟增加到50毫秒,并保持4天不变。在此期间,我们对应用的KPI进行监控,并将它们与我们的基准数据进行比较。我们发现,大部分API调用有轻微的减慢,不过都在可接受的范围之内,而且没有对用户体验造成影响。
这是一个里程碑式的阶段性胜利:我们确信我们的应用可以跨站点运行,也就是说,我们的“增速阶段性切换”方案是可行的。
负载均衡(从物理均衡器到HAProxy)
我们的数据中心运行着一个高可用的负载均衡器集群。在迁移到云端之后,就无法使用物理的负载均衡器,所以我们开始调研虚拟的负载均衡解决方案。
理想情况下,我们可以在GCP上部署单个负载均衡层。不过从现实来看,这个是不可行的。我们根据cookie、消息头和URL将请求消息路由到特定的分片上,但我们无法在GCP负载均衡平台上做这些解析工作。最后,我们基于Google网络均衡器和一个基于Linux的HAProxy集群构建了新方案。
Google网络均衡器将成为用户流量的入口点,并将流量均衡地分发到HAProxy集群上,然后HAProxy集群将流量路由到特定的分片上。
在通过实验测试之后,我们想使用真实的流量来对新方案进行测试。我们采取阶段性测试,在前一个测试通过之后才会增加更多的流量。到目前为止,后端的Evernote服务仍然在物理数据中心里运行,而跨站点的负载流量会通过私有VPN连接路由到那里。我们打算这样做:
- 将Evernote员工流量重定向到新的“前门”。我们更新了公司的DNS,从而将Evernote员工流量引导到新的前门。
- 使用Dyn Traffic Manager逐步将用户流量导入到新前门。
我们通过上述的两个方法对我们的新负载均衡平台进行测试。与跨站点测试一样,我们庆幸我们能够单独完成组件的测试,并确信这个方案是可行的。
Reco服务(从UDP到发布订阅)
当用户将附件或资源上传至Evernote,有个后端服务会尝试提取图像或PDF里的文本信息,这个服务被称为Reco(“recognition”的缩写)。
因为之前的架构存在种种限制,Reco服务器只能通过轮询的方式拉取资源。可以想象得到,多台Reco服务器定期向NoteStore服务器发起轮询,会给NoteStore服务器和资源存储服务器带来很大的压力。随着用户附件的增加,需要添加更多的Reco服务器,从而让情况愈加恶化。
为了减少因Reco服务器的增加而带来的开销和延迟,我们对Reco服务器进行了重新设计,通过使用多路广播,当NoteStore增加新的资源时,Reco服务器就会收到通知。
不过GCP的网络并不支持多路广播,所以我们使用新的通信模型对应用进行了重新设计。
我们移除了轮询机制和多路广播机制,取而代之的是基于发布订阅模型的队列机制,这种机制不仅可靠而且可伸缩。NoteStore向队列里发布任务,Reco服务器订阅队列,并获取任务,在处理完任务后进行确认。我们创建了多个优先级队列,Reco服务根据队列的优先级来处理任务。
我们使用基于云的队列机制极大简化了原先的架构,现在能够影响Reco服务的因素只有两个:队列里是否有需要处理的任务以及通知的速度。除此之外,我们正进一步改造Reco服务,让它支持自动伸缩,这样我们就可以把注意力放在如何管理和维护附件资源上。
用户附件存储(从多WebDev到Google云存储)
我们需要将120亿个用户附件和元数据文件从原先的WebDav服务器拷贝到Google云存储上。
因为数据量巨大,拷贝文件是整个迁移项目的关键一环。在进行拷贝的过程中,我们的服务仍然会对WebDav服务器读写数据。
我们碰到的第一个障碍是我们的网络,我们无法每天从几千个节点拷贝几百TB的数据。所以,我们不得不花一些时间建立了多条到GCP的网络通道。在进行数据拷贝的同时,我们要确保不会对自己造成DDoS攻击,还要保护好用户服务。
我们开发了一个Java应用程序,它可以直接运行在WebDav服务器上。WebDav服务器根据它们的物理RAID情况被拆分成目录树。资源迁移器遍历目录树,并将每个资源文件上传到Google云存储(GCS)。
为了确保文件能够上传成功,我们在本地为每个文件生成了一个散列值,这个散列值连同文件一起发送给GCS。GCS有独立计算散列值的功能,它将自己计算的散列值与收到的散列值进行比较。如果散列值不匹配,GCS会返回一个HTTP 400 BAD REQUEST错误码,资源迁移器会进行重试。如果连续几次重试失败,错误会被记录到日志里,用于后续的修复之用,而资源迁移器会继续上传其他文件。
通过性能测试,我们发现拷贝过程主要受RAID阵列的IOPS(每秒输入输出操作)和WebDav服务器CPU的影响。为了避免对用户体验造成影响,我们使用了两个并行的资源迁移器实例(每个RAID阵列使用一个迁移器),每个实例使用40个并行线程。这样我们就可以同时上传80个资源文件而不会对用户造成负面影响。
有了资源迁移器,接下来需要创建一个控制层来管理这些迁移器。
资源迁移器是一个小型的应用,WebDav集群上的每个目录树都需要一个迁移器实例。因为有几百个目录树需要迁移,所以需要一个控制层来管理这些迁移器。
我们在shell脚本里集成了现有的目录管理工具,实现了对资源迁移器实例的启动、停止和追踪。
因为受每个WebDav服务器最多只能运行两个实例和每个物理服务器机柜最多只能运行20个实例(受网络限制)的限制,迁移编排器必须能够智能地管理好资源迁移器实例,尽量减少人工的干预。
从高层面看,迁移编排器需要满足如下要求。
- 提供一个集中式的控制台用于管理所有的资源迁移器
- 维护任务清单,并识别出可迁移的任务(正在写入的目录是不能进行迁移的)
- 了解数据中心和主机的情况,避免出现资源过载或影响到生产流量
- 提供稳定的24/7吞吐和并发任务
如果速度全开,我们可以同时运行100到120个迁移器实例,这些实例完全由迁移编排器来控制。
接下来,我们要考虑如何更新我们的应用程序,以便从GCS读写数据。我们决定加入一些开关,用于切换GCS的读写功能。有了这些开关,我们可以先在部分分片上打开这个功能,让新功能的发布更安全、更可控。
将服务迁移到新的存储后端是一个敏感的操作。我们在开发环境和测试环境进行过大量的测试,在进入生产环境之前我们能做的也只有这些了。
为了做到干净安全的切换,并最小化对用户的影响,我们把迁移分成几个独立的步骤。为了方便理解,我们先来了解下最初的资源上传过程。
- 用户向Evernote服务发送保存资源的请求。
- 服务收到用户的资源,并启动两个进程:
- 确保资源的两个副本被保存到WebDav服务器上
- 将资源添加到异步任务队列
- 在完成了上述两个步骤之后,向用户返回保存成功的消息。
- 后台处理异步任务,资源的第三个副本被拷贝到远程的WebDav服务器上。
第一阶段
第一步,我们往异步队列里写入需要上传到GCS的资源。这样做有如下几个好处。
- 新的资源将会被自动上传到GCS,为我们节省了使用资源迁移器的时间。
- 另外,我们可以将生产环境的写入流量导向GCS API。这对于测试我们的代码是否能够处理大量数据来说非常关键。我们也因此对一些故障场景和GCS的评估有了更深入的了解。
- 更重要的是,以上过程全部发生在用户的视野之外。这里所发生的故障对于用户来说都是100%透明的,所以我们可以自由地进行迭代,不断改进我们的代码,而不会对用户体验造成负面影响。
到现在为止,我们对写入性能充满了信心,并对我们的代码和GCS有了正确的评估。下一步是如何让读操作的性能也能达到相同的等级。
我们伺机从GCS读取资源数据,如果有些资源还没有被迁移过来,或者出现了故障,我们的服务会立即转向主WebDav服务器读取数据。
因为下载资源处在用户的关键路径上,在读取资源时发生转向有可能会造成一些延迟。不过这些延迟一般在毫秒级别,用户感知不到。
头两轮的测试让我们对新的存储后端和代码有了更多了解。我们发现了一些罕见的故障(1:1,000,000的比率),而且发现GCS Java SDK内置的重试机制无法满足我们的要求。我们通过快速迭代改进了我们的代码,解决了这些故障,让服务更加健壮。
第二阶段
在经过几轮迭代之后,我们准备进行全面的提交。下一步要移除对WebDav服务器的依赖。不过,我们对资源的迁移流程还在进行中,所以伺机读取资源的机制还要保留,不过对于新的资源来说,就没必要再使用WebDev了。
首先,我们要改变资源上传流程。之前,资源会被上传到两个主WebDav服务器上,而现在它们直接被上传到GCS,而且我们将WebDav服务器的位置数量减至一个。
- 如果GCS发生重大故障,所有的重试都会失败,用户最终会收到“上传失败”的错误信息(不过我们的客户端会再次重试,所以不用太担心)。这也就是我们能得到的最糟糕的结果。
- 虽然我们减少了WebDav的写入,但并不会降低数据的持久性。GCS提供了更好的数据冗余,所以这是一个很大的进步!
- 减少WebDav数量有助于降低延迟(两次写入与三次写入的对比),同时,仍然保留单个主WebDav拷贝让我们感觉到很安全。
我们对系统的性能和健康情况进行了深切的监控,在运行了一段时间之后,我们准备全面停止向WebDav服务器写入数据。
- 禁用离岸灾备中心的异步备份作业(GCS不仅为每个资源维护多个副本,还会将它们存储在不同的地理区域,为我们提供了很好的弹性,以便应对灾难性故障和自然灾害)。
- 禁用主WebDav写入。
现在的上传流程更简单了。
- 用户像Evernote服务发送保存资源的请求。
- 服务接收资源并将其写入GCS。
- 服务向用户返回成功信息。
在后台,GCS负责保存资源的多个副本,并保存在多个地理位置。
在撰写本文时,我们已经完成99.2%的资源迁移,这一过程是整个迁移项目非常重要的一部分。可以说,我们即将看到胜利的曙光!
GCS伺机读取机制还会继续存在,直到我们进行一次最终的完整性检查,确保所有的资源都已成功迁移。
为了完成最终的完整性检查,我们遍历了资源数据库,然后向GCS查询这些资源是否存在,并再次验证每个文件的散列值。与资源迁移过程相比,完整性检查完成得会快很多。我们按周而不是按月来检查资源,确保120亿个资源都安全地进行了迁移。
进行云端分片测试也需要经过一些步骤。在迁移项目启动之初,我们就定义了一些迭代流程用于测试运行在GCP上的Evernote服务。在进行全面迁移之前,我们需要确保在用户负载全开的情况下,单个分片能够成功地运行在GCP上。我们要找出之前的测试没能覆盖到的边界情况。
在感恩节之前,我们成功地将两个用户分片迁移到GCP上,并让它们运行了几个小时,最后再回退到物理数据中心。我们完成了最后的预迁移检查,为后续的全面迁移亮起了绿灯。
在计划Evernote服务迁移的过程中,我们希望尽量最小化宕机时间,不过我们也知道,我们现有的架构不支持零宕机迁移。在制定迁移方法和维护时间窗口的计划时,我们提出了一些要求。
- 我们要最小化在“多站点”模式下运行的时间。
- 从迁移单个分片开始,随后逐渐增加到同时迁移8个分片。
- 在少量分片迁移到GCP之后,我们需要大概48小时的时间用于观察它们的运行状态。
- 最多同时支持迁移32个分片,再多就会有风险。如果出现问题,涉及的范围越大就越难以控制。
- 选择在周末时间迁移UserStore,并把迁移时间保持在一个小时以下。因为UserStore的迁移会造成全面宕机,所以我们要最小化对用户造成的影响。
- 每个分片的迁移造成的宕机时间不能超过30分钟。宕机从来都不是一件好事,这也是我们一直要避免的。可惜我们的应用不支持透明的分片失效转移。不过好在用户客户端内容的同步间隔一般在15分钟以内,所以用户可能不会注意到宕机。
- 我们希望有足够的灵活性,以便在迁移过程中应对一些未知的状况。
- 一旦全局服务正式运行,就不再允许出现任何“停业整顿”之类的维护时间了。
基于上述几点考虑,以及我们对分片迁移速度的预期,我们最终做出了如下计划。我们在论坛和社交媒体上与用户就此事进行了沟通。
分片的迁移时间为每天的太平洋夏令时时间,从早上9点到晚上9点。
- 12月8号到9号进行第一次分片迁移
- 12月11号进行UserStore迁移
- 12月14号到16号进行第二次分片迁移
从12月16号到31号,我们耐心观察等待。所有的运维工程师24/7待命,以便快速对问题和故障做出响应。
从12月1号开始,我们也一直在迁移那些不会影响用户体验的服务,比如图像识别服务和前端流量。
NoteStore
在计划迁移NoteStore时,我们利用到了之前的“分片迁移”经验。之前,为了在数据中心的不同硬件间移动分片,我们开发了一套流程和脚本。虽然这套流程不能完全适用于这次迁移,但仍然成为这次迁移的核心基础。
首先,我们要在GCP端创建新的分片,然后开始同步数据。
我们使用Google的部署管理器和我们已有的Puppet脚本创建新的NoteStore。这些新分片的名字和配置与生产环境中的分片是一样的,只是没有用户数据,而且还没有被配置到负载均衡平台上。
每个分片有两种用户数据需要同步:
- 存储用户笔记的MySQL数据库
- 复制生产环境MySQL最新的备份
- 处理备份
- 解压
- 解密
- 启动数据库实例
- 在源实例和目标实例上配置用于复制的用户角色
- 从备份点开始复制
- Lucene索引。Lucene与MySQL不一样,它并没有提供原生的数据复制支持,所以我们需要使用自定义工具来移动数据。我们基于ubiquitous rsync utility创建了一个工具,并通过cron在每台源主机上运行,向我们的Graphite/Splunk数据收集管道发送度量指标和日志。
我们在切换分片服务之前的5到10天启动数据复制流程。
在这个过程中,我们不断迭代更新我们的方法。
- 最初的MySQL配置使用日常备份和二进制日志进行数据复制。不过后来发现,我们的备份服务器无法满足多个进程并行运行的要求。
- 为了满足我们的并行要求,我们使用新的GCS bucket作为备份目标。我们通过重写ansible playbook进行基于流的innobackupex加密和压缩备份,并将其直接写入GCS bucket。
- 在每个分片的备份可用之后,我们使用新的playbook在复制目标上挂载更多的PD存储,用于下载、解压备份数据。
- 另一个playbook会运行我们在之前的迁移中开发的工具“genesis”,这个工具会验证MySQL的配置、恢复备份、更改密码、启动和关闭检查、升级数据库,并最终启动MySQL复制进程。
- 我们的Lucene复制工具很稳定,不过很快我们就发现我们的交换机链路和数据中心到GCP之间的VPN连接逐渐趋于饱和。因为一些技术原因,我们不得不调整最初的Lucene复制进程数量(最初的同步复制严重消耗网络带宽,经过调整之后,后续的同步只复制变更的部分)。
- 我们对网络通道映射也做了一些调整,让ansible按需进行批次复制,先是观察初始的复制情况,再通过cron运行新的复制。
服务切换的目标是尽快安全地完成切换,以便最小化宕机时间。
- 停止生产环境的分片,并将其移出负载均衡器,确保用户不会对其做任何更新。
- 运行最后阶段的增量数据同步。
- 启动新的分片(包含了同步过的数据)。
- 使用一套工具(我们把它们叫作“confidence”)检查数据是否复制成功,是否是最新的,校验和是否匹配,等等。
- 在新分片上运行QA脚本,验证分片的功能是否正常。
- 移除新分片的备份。
- 新分片向用户开放。
- 关闭日志、事务和告警监控。
为了加快速度,我们不能依靠手动工作。在确保这个流程可行之后,我们使用ansible对流程进行自动化。
我们的脚本可以在30分钟内自动迁移一个分片(平均包含了30万个用户的数据)。我们逐渐增加并行数量,直到可以并行迁移32个分片。这样我们就可以在30分钟内迁移大约1千万用户的数据。
在迁移过程中,来自运营团队的不同角色的人员一起协作,确保一切进展顺利。
项目经理:项目经理协调整个迁移项目,并作出决策,包括批处理的大小、何时开始批处理以及如何与外部沟通进度。
首席工程师:首席工程师对迁移有完全的控制权,他在迁移之前坚定大家的信心,管理迁移工具,并作出排查决策。
监控工程师:监控工程师查看我们的监控系统,找出异常情况,并在每个迁移完成之后通知大家。他们还会运行QA工具,最终确认迁移是否成功。
修复团队:我们有2到4个工程师排查和修复由首先工程师和监控工程师发现的问题。
UserStore
UserStore的迁移步骤与MySQL的迁移基本步骤是一样的,不一样的地方在于需要将原先指向NoteStore的JDBC URL改成指向新的主机。
因为UserStore处在整个Evernote服务的中间,所以我们在一个周日早上特意安排了一个小时的维护时间。因为UserStore承担了整个服务的大部分事务,为了监控迁移过程,我们让一大组运营人员和工程师在办公室待命。还有一个来自Google的团队,他们也做好准备,以便应对可能出现的问题(他们还为整个团队带来了早点)。
这也许算得上整个迁移过程最为紧张的一小时,整个服务出于停机状态,时间在一分一秒地流逝。
其他服务
其他服务的迁移会简单一些,我们可以在迁移的同时创建新的服务,并逐步完成切换。我们可以重用一部分已有的Puppet代码,并借机对它们进行清理和简化。
总得来说,我们基本是按照最初的计划进行的,尽管曾经有两次偏离了计划。
- 为了更新配置参数,我们在12月15号重启了UserStore,确保它们在新的环境中能够正常运行。
- 我们使用了额外的一天时间(12月19号)用于迁移最后的96个分片。我们本来可以很快地迁移这些分片,不过最后还是决定采取更安全的方式,为此多花了一天时间。
关于这些变更,我们都有在论坛和社交媒体上与用户进行过沟通。
分片迁移过程中的数据
下图的气泡代表迁移中的用户批次,气泡越大,用户批次就越大。Y轴表示每分钟迁移的用户数量,X轴表示批次。不同颜色代表不同的迁移工作日。
- 11月23号,我们进行尝试性的用户分片迁移,用于确定最终的流程和工具。
- 12月8号,没有迁移分片,因为我们发现了一些代码缺陷需要修复。
- 12月9号,开始小规模的迁移。
- 12月14号,我们开始大踏步,在一天内迁移了7千7百万个用户。
批次统计
- 最大的一个批次是33号,在24分钟内迁移了9,463,546个用户(每分钟394,314个用户)。
- 最快的一个批次是37号,在14分钟内迁移了9,244,151个用户(每分钟660,297个用户)。
- 耗费时间最短的一个批次是43号,在11分钟内迁移了4,042,851个用户(每分钟367,532个用户)。
- 耗费时间最长的一个批次是51号,在30分钟内迁移了147,765个用户(每分钟4,926个用户)。
- 所有用户的服务不可用时间都控制在30分钟以内。
极度聚焦
为了达成我们的目标,我们尽量排除外界的干扰,避免浪费时间。我们不得不拒绝那些不重要的业务任务,而且要让整个公司都知道我们这样做的理由。我们通过分享会议让其他人了解我们的项目,同时给他们机会提问。
给力的伙伴
在项目开始的时候,我们讨论如何快速地完成迁移,我们希望避免平庸,所以设定了非常高的目标。与我们一起合作的Google团队对于我们提出的挑战也毫不畏惧。每次在我们发现可能导致偏离目标的问题时,Google团队总能站出来一起解决。
为了完成共同的目标,来自两个不同公司的团队在一起合作,这是一件非常有意思的事情。
让事情落地,并尽快完成
传统的思维告诉我们,对于这类迁移,应该从小处着手。某种程度上说,这是对的,不过我们不能完全依照这种说法去做。我们认为我们应该快速完成迁移,并把迁移看作整个环境的组成部分,因为这是让所有潜在问题浮出水面的唯一方法。
从小处着手,或者从非关键路径开始迁移存在一些根本性的问题。
- 规模太小的东西一般不会暴露出什么问题,即使我们尽可能地进行基准测试或负载测试,也无法发现可能会在生产环境发生的问题。它们只会降低我们对安全的防范意识。
- 我们一般不太关注非关键的东西(不太重要的),所以即使出现问题也能接受。这些问题一般得不到修复,一旦进入生产环境就会给我们带来危害。
不过,从小处着手可以帮助我们熟悉新的平台,并做出一些基本的测试。这一步是必须去完成的,不过这并不代表在进入生产环境之后一切能够正常运行。
在迁移过程中,我们尽可能快速地对生产环境服务进行测试。如果我们的系统或应用能够被拆分成组件进行迁移,对于测试来说就非常有利。基于微服务的架构在这方面极具优势,不过对于古老的单体应用来说就非常困难。
简而言之,处在用户关键路径之外的高流量组件是最好的着手点。
自动化
我们尽可能借助自动化加快迁移过程。很多时候,我们使用已有的工具。不过基于一些原因,我们也会自己构建一些工具。
- 在GCP里创建实例。我们基于Google云部署管理器创建自定义工具,用于创建配置模板,并利用现有的Puppet基础设施进行配置管理。
- 我们创建了迁移脚本和整体控制框架,实现了24/7的高速文件拷贝。我们有大约50亿个用户文件需要拷贝,而每个用户文件又有相关的元数据文件(如缩略图),所以总的文件数量达到120亿个。
- 我们创建了基于Ansible的工具,用于初始化数据的迁移流程,并实现了分片迁移的完全自动化。
如果不注重自动化,根本不可能在如此短的时间内完成迁移。
打破约束
在制定迁移计划时,我们意识到数据中心和GCP之间的网络带宽会对我们造成约束。它不允许我们拷贝大块的数据,也限制了多站点运行能力。
我们知道网络约束会给我们的工作带来多个负面影响。于是在项目早期阶段,我们花了额外的时间和精力最大化站点间的带宽。我们对比了几个云供应商,并最终做出了选择,为我们节省了宝贵的时间。
总得来说,我们对迁移过程感到很满意。我们也从中学到了很多。
云服务的成熟度已经达到一定高度。在大多数情况下,我们的应用可以正常运行在云端。不过,我们仍然需要“调节”我们的应用,因为在云端环境可能会受到一些约束。所以我们要提前做好准备,在迁移之后需要留出“调节”时间。
例如,为了解决磁盘吞吐量受限问题,我们不得不调节我们的运行环境。在我们的数据中心,我们可以尽快地运行备份,因为我们的环境或存储设备没有被共享,所以可以尽情地消耗所有可用的磁盘IO。但在GCP上,我们的自动化备份会让虚拟机的IO达到顶峰,从而拖慢了上游应用。后来我们通过限定备份的速率来解决这个问题。
我们还通过调节环境来实现实时迁移。这是Google后端的一个特性,允许在主机间迁移虚拟机,而且在大多数情况下对用户是不可见的。UserStore和部分比较活跃的分片因为IO很高,在进行实时迁移时不太顺畅,即使是一段很短时间的不可用也会造成破坏性的中断。我们通过调节MySQL和应用来解决这个问题。尽管还不完美,但我们仍然会继续在Google的协助下不断完善它。
我们需要与用户沟通有关迁移维护的事情。我们不希望给用户发送大量的邮件,因为他们很可能不会查看邮件。我们希望通过Evernote社区论坛和社交媒体就维护问题与用户进行沟通。这种方式确实奏效了,不过还是有一小部分用户因为不知道这些事情而受到了影响。我们希望消除这个问题,所以在未来,我们会考虑使用应用内置的方式与用户进行沟通。
总结
在新的环境里,我们总不可避免地会遇到各种小问题。有些问题还比较棘手,不过通过调整已有的配置,并与Google团队展开通力合作,这些问题最终都得到了解决。
我们完成了整个迁移,现在可以展望下未来。我们之所以要迁移到云端,是为了让我们的工程流程更加顺畅,让我们的工程师可以使用更多的构建工具。我们知道,随着时间推移,我们将会逐渐远离虚拟机,并逐步拥抱容器和无服务器技术,从而把精力聚焦在重要的事情上。
Evernote Hack Week
Evernote每年都会有一个Hack Week(一般在1月份)。对于公司里的每一个人来说,这是一个展示他们创造性想法的好机会。人们爆棚的创造力给我们带来惊喜,团队和想法在自由的环境里自主成形。
在过去,我们的运营团队会花时间来支持这个活动,他们需要为活动提供服务器和其他东西。不过今年很不一样,我们给了工程师单独的GCP项目访问权限,并让他们自由发挥。他们可以轻松地为自己的项目配置基础服务,尽可能少给运营同事增加负担,这样运营同事们就可以专注于迁移工作。
微服务平台
我们在去年还决定了要把我们的单体架构迁移到微服务架构,而使用容器是完成这个目标的第一步。我们做了很多调研,最终选择Kubernetes作为编排框架。运行生产级别的Kubernetes集群是一个艰巨的任务,所幸的是,Google Container Engine(GKE)简化了整个流程。
现在,我们在生产环境运行了一个GKE集群,为我们的下一代搜索基础设施提供支持。我们会在另一篇文章中详细介绍这方面的内容。
无服务器的未来
尽管我们对当前的架构进行了改进,为了能够运行Evernote的代码,仍然有上千个操作系统需要安装。理想情况下,我们应该只负责部署代码,然后让Google负责运行这些代码。
也就是说,我们只要负责使用计算能力来处理用户请求。不过我们还有很长的路要走,我们正在尝试使用Google Cloud Functions作为我们的核心构建块,这样我们就可以专注在用户代码和特性的开发上。
如何为云项目命名?
每个伟大的项目都需要一个名字。经过深思熟虑之后,我们从两个备选方案中选出一个作为项目的名称。我们的软件工程团队提议把这个项目叫作“Cloudy Meatballs”,其灵感源于Judi Barrett的著作《天降美食》(Cloudy with a Chance of Meatballs)。我们的运营团队则提议“Bob Ross”,源于著名的艺术家和电视名人Bob Ross,他能够画出蓬松的云彩。最后我们通过整个公司的投票来决定,“Bob Ross”胜出。
查看英文原文:Cloud Migration