每个人的电脑上都有一个被数百个进程使用的文件系统。但是,如果要让成千上万的用户同时操作数亿个文件,这些文件又包含了 PB 级的数据,那就困难了。一个是磁盘空间不足,一个是无法进行弹性扩展,一个是如果运行一些 IO 密集型的进程或者与其他多个用户协作就会出现瓶颈。在这篇文章里,我们把场景转到一个有数百万个付费用户的云原生文件系统上,你将看到我们如何使出浑身解数来扩展这个系统,让它支持持续增长的用户以及 SLA 需求,同时还能提供类似本地文件系统那样的体验。
Egnyte 是一个安全的内容协作和数据治理平台,创立于 2007 年。当时,谷歌还没有推出 Google Drive,AWS S3 的价格还非常昂贵。我们唯一能做的就是撸起袖子,开发了一个最基础的云文件系统。随着时间的推移,S3 和 GCS 的成本降到了可接受的范围,Egnyte 的存储插件架构也渐渐成熟,我们的客户可以选择他们想要的存储后端。在过去几年中,为了帮助客户管理持续增长的数据,我们重新设计了很多核心组件。在这篇文章里,我将分享我们当前的架构、在扩展系统过程中学到的东西以及一些在未来可改进的地方。
注:本文第一、二部分主要枚举了 Egnyte 公司的技术选型方案,包括云平台、编程语言、数据库、存储、服务器、负载均衡、部署管理、开发环境等方面。第三部分是更为详细的采访细节部分。
Egnyte Connect
Egnyte Connect 是一个内容协作和数据管理平台,组件包括 CFS(Cloud File System,云文件系统)、EOS(Egnyte Object Store)、内容安全、事件同步、搜索服务、基于用户行为的推荐服务。
Egnyte Connect 的 3 个数据中心负责处理来自全球数百万个用户的请求。为了保证弹性、可靠性和持久性,这些数据中心通过安全的 Google Interconnect Network 连接到谷歌云平台。
Egnyte Connect 的服务网格将我们的数据中心与谷歌云平台的多个服务连通起来:
协作
- 文档存储
- 预览
- 视频转码
- 分享(链接和权限)
- 标签
- 注解
- 任务
- 推荐
混合同步
基础设施优化
- 迁移到云端
- 优化本地冷存储成本
- 合并存储库
- Egnyte Connect 的架构会基于不同的级别来分片和缓存数据:
- 数据量
- 数据独立性
- 并发读
- 并发写
Egnyte Connect 的技术栈
云平台
编程语言
对象存储
- Egnyte Object Store
- GCS
- AWS S3
- Azure
应用程序服务器
数据库
- MySQL
- Redis
- BigTable
- DataStore
- Elasticsearch
缓存
负载均衡器和反向代理
消息队列
- 谷歌 PubSub
- RabbitMQ
- Scribe
- Redis
部署管理
- Puppet
- Docker
- Ansible
- Jenkins
- GitLab
- Kubernetes
分析
- New Relic
- OpenTSDB/bosun
- Grafana
- MixPanel
- Tableau
- BigQuery
其他
- ZooKeeper
- Nagios
- Apache FTP 服务器
- Kong
- ReactJS/Backbone/Marionette/JQuery/NPM/Nightwach
- Rsync
- PowerDNS
- Mashery
- 基于 REST API 的 SOA 架构
- 使用 Java 开发核心文件系统代码
- 使用 Python 开发面向客户端的代码,包括微服务、迁移脚本、内部脚本
- 原生 Android 和 iOS App
- 原生桌面和服务器端托管的客户端,可以混合同步访问整个用户空间
数据统计
- 3 个主区域,其中一个在欧洲,通过 Google Interconnect 连接到 GCP
- 500 多个 Tomcat 实例
- 500 多个存储节点(Tomcat/Nginx 提供支持)
- 100 多个 MySQL 节点
- 100 多个 Elasticsearch 节点
- 50 多个文本抽取服务实例(可以自动伸缩)
- 100 多个 HAProxy 实例
- 其他类型的服务实例
- 数十 PB 数据,保存在我们自己的服务器以及 GCS、S3 和 Azure Blobstore 上
- 数 TB 被索引到 Elasticsearch 中的内容
- 数百万个客户端与云端同步文件,用于离线访问
- 数百万个客户端通过交互式方式访问文件
开发环境
- 服务器安装了 Ubuntu
- UI 团队使用的是 Windows/Mac,他们连接到本地 Ubuntu 虚拟机或者共享的 QA 服务器进行 REST API 测试
- Eclipse/IDEA
- 构建环境在 AWS 上
- Maven
- Docker
- GitLab
- Jenkins
- Confluence
- JIRA
- 谷歌办公套件
- Slack
以下摘录了与技术和架构相关的问答
你们的系统是用来做什么的?
Egnyte Connect 有数百万用户,他们把它作为安全的内容平台,用来管理他们的文档。平台提供了各种各样的文件服务,并支持已有的云端文件系统。用户可以通过多种方式访问平台,比如 FTP、WebDAV、移动客户端、公共 API、浏览器。平台还提供了强大的审计和安全组件。
你们为什么要开发这个系统?
2007 年,我们的业务模式朝着分布式方向发展,用户通过多个设备访问他们的文件,这就有必要为用户提供一种顺畅的访问体验。于是,我们开发了 Egnyte Connect,一个分布式文件系统,支持混合同步,满足各种内容协作需求。随着本地数据和云端数据的碎片化以及 GDPR 的推出,我们开发了 Egnyte Protect,帮助用户实现数据的合规和治理。
你们的系统有多大?
我们的系统保存着数十亿个文件,数十 PB 的数据。Egnyte Connect 每秒钟处理 1 万个 API 请求,平均响应时间小于 60 毫秒。用户可以从三个不同的区域访问我们的系统。Egnyte Protect 提供了持续的内容监控能力,保证数据的合规和安全。
你们系统的架构是怎样的?
我们采用了基于 REST 的 SOA 架构,可以独立伸缩每一个服务,还可以将后端服务部署在云端。所有服务都是无状态的,它们使用了数据库或者我们自己开发的对象存储。
Egnyte Connect 服务概览:
请求流程概览:
搜索架构概览:
你们在设计、架构和实现系统时遇到了哪些特别的挑战?
较大的一些架构挑战包括:
- 文件存储的伸缩
- 元数据访问的伸缩
- 与桌面客户端实时同步
- 带宽优化
- 故障隔离
- 缓存
- 发布新特性
你们是如何解决这些问题的?
- 在存储方面,我们开发了自己的存储系统和插件架构,可以支持公共云存储,如 S3、GCS、Azure……
- 在元数据伸缩方面,我们改用 MySQL,并启用了分片。
- 在实时同步方面,我们修改了同步算法,就像 Git 那样,让客户端接收增量事件,并最终保持与云端状态一致。
- 在发布新特性方面,我们开发了一个自定义配置服务,提供了功能开关。这样就可以在睡眠模式下运行和收集数据,用户可以自己启用新功能,或者由某个 POD 或某个数据中心为一群用户启用新功能。
- 还有很重要的一点是监控。如果没有监控数据,就无法进行优化。有时候,我们监控的东西太多,以至于不知道该把重点放在哪里。所以,我们不得不转移注意力,使用其他工具(比如 New Relic、bosun、ELK、OpenTSDB 和自定义报告)来检测异常。
你们是如何演化系统来应对这些挑战的?
- 第一个版本:用 Lucene 索引文件元数据,把文件保存在 DRBD 中,通过 NFS 挂载,然后在 Lucene 中搜索。问题:Lucene 的更新不是实时的,所以需要被替换掉。
- 第二个版本:用 Berkeley DB 保存文件元数据,把文件保存在 DRBD 中,通过 NFS 挂载,然后在 Lucene 中搜索。问题:达到 NFS 的限制,需要被替换成 HTTP。
- 第三个版本:用 Berkeley DB 保存文件元数据,把文件保存在 EOS 中,通过 HTTP 连接,然后在 Lucene 中搜索。问题:在大流量压力下,启用了分片的 Berkeley DB 仍然会出现瓶颈,而且一旦数据库发生崩溃,需要几个小时才能恢复,所以需要被替换掉。
- 第四个版本:用 MySQL 保存文件元数据,把文件保存在 EOS 中,通过 HTTP 连接,然后在 Lucene 中搜索。问题:公有云变得越来越便宜了。
- 第五个版本:用 MySQL 保存文件元数据,把文件保存在 EOS/GCS/S3/Azure 中,通过 HTTP 连接,然后在 Lucene 中搜索。问题:搜索遇到瓶颈,需要被替换掉。
- 第六个版本:用 MySQL 保存文件元数据,把文件保存在 EOS/GCS/S3/Azure 中,通过 HTTP 连接,然后在 Elasticsearch 中搜索。这是目前的架构。
- 第七个版本(未来):把所有的计算移到云端,拆分出更多的服务,实现故障隔离,使用动态资源池更好地管理资源。
你们使用了哪些很酷的技术或者算法吗?
- 对于服务间调用,我们使用了指数回退,实现了回路断路器,避免出现故障雪崩。
- 核心服务节点资源使用了公平共享的分配方式,接收到的请求被打上标签并分组。每一组都有一定的容量,假设有一个客户每秒钟发送 1000 个请求,其他客户每秒发送 10 个请求,系统可以保证其他客户不会因为这个客户发送太多请求而“挨饿”。这里的诀窍在于,当只有一个用户在使用系统时,它可以开足马力,随着用户越来越多,它们可以共享容量。对于大客户,我们创建了专门的资源池来保证一致的响应时间。
- 一些有 SLA 要求的核心服务使用了单独的 POD,保证不让坏客户影响了整个数据中心。
- 我们使用了基于事件的同步机制,当服务器端发生了事件,事件被推送给桌面客户端,客户端在本地重放这些事件。
- 我们采用了大规模数据过滤算法,让大集群的客户端与云端文件系统进行同步。
- 对于不同的问题,我们使用了不同的缓存技术,包括:
传统的缓存技术。
简单的内存缓存,包括可变对象和大数据集合。
复杂的内存缓存,比如高容量可变数据集合。
基于磁盘的缓存。
你们有哪些独到的东西是值得别人学习的?
初创公司要把注意力放在核心技术能力上,如果遇到了技术难题,需要自己开发一些东西,那么就撸起袖子干吧。有很多东西可以学的,其中存储层、基于事件的同步机制最值得一看,更多细节可以参看:
你们总结了哪些经验?
- 尽可能收集系统相关信息,先优化经常被使用的部分。
- 在刚开始引入新技术时不要太过激进,不要奢望一开始就为手头的问题找到完美的工具。如果引入了太多技术,代码写起来虽容易,但维护工作(比如部署、运维、学习曲线)会变得困难。随着规模的增长,需要把系统拆分成多个服务。保留一两个服务作为微服务模板,尽量不要使用不同的技术栈来开发不同的微服务。
- 作为初创公司,你需要小步快跑。先引入当前最为合适的方案,然后一步步加以改善。
- 找到单点故障点,并毫不留情地把它们一网打尽。付出额外的努力去解决那些让你夜不能寐的问题,并尽快从防守转为进攻。
- 在 SOA 架构中,尽早使用回路断路器来减少负载,如果服务达到瓶颈,可以发送 503 错误码给客户端。与其拒绝处理每一个请求,不如看看是否可以公平地分配资源,只拒绝那些滥用资源的请求。
- 给服务消费者添加自动修复功能。服务器可能会发生阻塞,桌面客户端或其他服务消费者可以通过指数回退降低服务器端的压力,并在服务恢复时自动修复。
- 始终可用:使用服务端回路断路器和消费端回路断路器。例如,如果通过 WebDAV 或 FTP 访问文件系统存在性能问题,并且需要 4 个小时来修复,那么在这 4 个小时中,可以在网关或防火墙上禁用 FTP/WebDAV,并要求客户端使用 Web UI 或其他方式。类似地,如果一个客户端的异常行为阻塞了系统,就暂时禁用这个客户端或者相应的服务,并在问题修复后重新启用。我们在这方面使用了功能开关和回路断路器。
- 对于高度可伸缩的服务,让它们运行在 Java 进程之外需要付出很高的成本,甚至放在 Memcache 或 Redis 中也一样,所以我们在内存中缓存了一些使用率很高的数据结构,如访问控制计算、功能开关、路由元数据等,并设定了不同的 TTL。
- 在处理大数据集时,我们发现了一些模式。无休止地优化代码可能是徒劳的,那样只会让代码变复杂。通常情况下,最简单的解决方案来自于最初的想法:
减少所需的数据集;
重新规划数据在内存或磁盘上的存储方式;
反规范化数据,避免使用表连接;
使用基于时间的过滤器,比如归档旧数据;
使用多租户数据结构来创建更小的分片;
通过事件来更新缓存,而不是进行全量更新。
- 保持简单:每个月都会有新人加入,所以我们的目标是让他们能够在第一周就上手,而只有简单的架构才能实现这个目标。
你们的开发流程是怎样的?
我们实行 Scrum,云文件系统团队每周发布一次版本。我们使用了 Git Flow 的一个变体,对于每一个 ticket,我们克隆代码库,并为每一个合并请求执行自动化测试。一个合并请求需要由两名工程师确认,之后相应的 JIRA ticket 才算解决。ticket 被解决之后会进入后续的发布流程,包括自动化 REST API 测试和一些手动冒烟测试。
代码在发布前的 2 到 3 天进入 UAT 环境,继续发现在自动化测试中没有被发现的问题。我们每周三正式发布版本,每天生成异常报告。我们把发布时间选在这个时间点,主要考虑到了生活和工作的平衡,因为一旦发布出了问题,所有的工程师都在。
你们的服务是怎么划分的?
我们采用了 SOA 架构,根据服务的类型来分配服务器,其中顶级服务包括:
- 元数据
- 存储
- 对象服务
- Web UI
- 索引
- 同步
- 搜索
- 审计
- 内容智能
- 实时事件传递
- 文本提取
- 集成
- 缩略图生成
- 反病毒
- 垃圾邮件
- 预览 / 缩略图
- 远程备份
- API 网关
- 计费
- FTP/SFTP
- 其他
你们是怎么分配服务器的?
大部分服务运行在虚拟机中,只有一小部分运行在物理机上,比如 MySQL、Memcached 和存储节点。我们使用了第三方基于模板的服务器分配工具。不过,我们已经着手把所有东西都迁移到云端,所以最后会使用 Kubernetes。我们面临的最大挑战是如何在不宕机的情况下做这些事情。
你们使用什么数据库?
MySQL 和 Redis。之前我们也使用了其他数据库,比如 Berkeley DB、Lucene、Cassandra,然后出于工程和运维方面的考虑,逐步改成 MySQL。
在某些地方我们还使用了 OpenTSDB、BigTable、Elasticsearch。
你们的存储策略是什么?
刚开始我们自己组建服务器,在一台机器上使用尽可能多的磁盘,因为那个时候 AWS 的价格还很昂贵。我们也尝试了 GlusterFS,但它的伸缩性满足不了我们的需求。当 S3 变得越来越便宜,GCS 和 Azure 也开始出现,我们重构了存储层,支持插拔,用户可以选择他们想要的存储引擎(Egnyte、S3、GCS、Azure 等)。现在,我们在云端和数据中心分别保存一个副本,最终会使用数据中心的副本作为直通缓存,因为云服务虽然便宜,但带宽仍然很贵。
你们是如何规划容量的?
我们有半自动化的容量规划工具,根据 New Relic、Grafana 和其他统计信息来规划容量。我们每个季度都会基于监控报告的关键指标进行容量规划,并预留一些容量。一些服务部署到了云端,可以根据队列大小进行自动伸缩。
你们的数据库架构是怎样的?主副?分片?其他?
对于大部分数据库,我们采用了主副复制的模式,并带有自动失效备援机制。有些变动频繁的数据库需要进行手动切换,因为复制延迟会导致在切换时应用程序数据不一致,所以我们需要通过重构部分核心文件系统逻辑来修复这个问题。
你们是怎么解决负载均衡问题的?
我们根据用户访问系统时使用的 DNS IP 将流量路由到相应的数据中心,然后根据 HAProxy 将流量路由到相应的 POD,在 POD 里也是根据 HAProxy 进行路由。
你们的系统提供了标准的 API 了吗?如果有,你们是怎么实现的?
我们的 API 分为 3 类:
- 公共 API:为第三方 App 开发者、集成团队和我们自己的移动 App 提供的 API。
- 客户端 API:为我们自己的客户端提供的 API。
- 内部服务 API:数据中心内部使用的 API,服务之间通过这些 API 相互通信,外部无法调用这些 API。
你们的对象和内容缓存策略是怎样的?
我们保存着数 PB 的数据,所以不可能缓存所有的东西。不过,如果一个用户有 5 千万个文件,那么在 15 天内他可能只会使用其中的 1 百万个。我们有基于 LRU 算法的缓存过滤器节点,可以弹性地增加或减少节点数量。上传速度是一个大问题,该如何保证从世界各地将文件快速上传到 Egnyte?为此,我们构建了特殊的网络连接点(PoP)。
我们使用 Memcached 和 Redis 来缓存元数据,Memcached 缓冲池用来保存长期使用的静态数据和文件系统元数据。核心文件系统的元数据很大,Memcached 节点放不下,为此我们使用了三种缓存池,并在应用层决定该从哪里获取需要的数据。不同类型的数据具有不同的过期时间。对于某些请求(如列出文件内容),将一些经常用到的数据(如客户信息或分片映射信息)放在 Memcached 中反而会让速度变慢,为此,我们将这些数据放在 JVM 的内存中,并基于 TTL 或者发布订阅机制来冲刷它们。
缓存方面存在的两个最大的问题是权限数据和事件数据。对于权限数据,我们已经进行了多次重构,最近开发了 TRIE 来缓存它们。
我们把事件数据缓存在 Memcached 中,但在某些情况下会出问题。例如,我们可能在夜间为一个用户发布 10 万个事件,到了早上 9 点,有 3 万个用户打开了他们的电脑,每个用户都需要接收着 10 万个事件。也就是说,我们需要在短短的 15 分钟内处理 300 亿个事件,而且只能将事件发送给有权限访问这些事件的用户。因为事件是不可变的,我们可以将它们保存在 Memcached 中 12 个小时,但多次下载事件数据仍然存在网络方面的压力。最终,我们将事件缓存在内存中一小段时间,并调整了 GC 设置。我们还将这些节点放到更快的网段中。不过,这个问题还没有完全解决……
你们是如何检测全局可用性以及如何模拟终端用户性能的?
我们的节点分布在不同的 AWS 区域,以此来测试带宽性能。我们还基于内部 HAProxy 报告来调整上传 / 下载速度,并使用特殊的网络 PoP 和其他策略来加速数据包的传输。
你们是如何进行服务器和网络可用性检测的?
我们使用了 Nagios、Grafana、New Relic 和一些内部异常分析工具。
你们是如何可视化服务器统计信息和趋势的?
我们使用了 Grafana、Kibana、Nagios 和 New Relic。
你们是如何测试系统的?
Selenium、JUnit、Nose、Nightwatch 和手动测试。我们会进行单元测试、功能测试、集成测试和性能测试。
你们是如何分析性能的?
我们使用 New Relic 来监控应用程序性能。我们还使用自己开发的框架收集了很多应用程序指标。我们也使用了 Grafana、Nagios、Kibana 和一些内部工具来监控系统其他部分的性能。
你们是如何处理安全问题的?
我们有专门负责安全的团队,他们在每次发布版本之前都会运行自动化的安全基准测试。我们在生产环境中持续运行自动化渗透测试。我们还推出了 bug 奖励计划。一些客户借助第三方工具自己进行安全测试。
你们是怎样备份和还原系统的?
我们使用 Percona XTraBackup 来备份 MySQL。Elasticsearch 会被复制 3 份。用户的文件也会被复制 3 分,其中一份保存在云端。如果一个副本无法还原,我们就将其放弃,并加入新的副本。对于某些用户,我们会额外地复制一份到他们选择的存储厂商。对于选择 S3、Azure 或 GCS 作为存储的用户,我们会确保为它们复制了一个副本,防止数据丢失。
你们是如何处理数据库结构变更的?
不同的服务使用了不同类型的数据库,它们的升级方式也不一样。
- EOS 保存了对象元数据,增长速度很快。我们对它进行了分片,并不断加入新分片。
- MDB 增长速度更快,我们也对它进行了分片,并不断加入新分片。
- dc_central 是一个 DNS 数据库,相对比较稳定。出于伸缩性方面的考虑,我们复制了多个副本。
- pod_central 保存频繁变化的数据,但每张表不会超过 20M。出于伸缩性方面的考虑,我们也复制了多个副本。
- 每个数据库的 schema 都保持向前和向后兼容,也就是说,我们不会在同一个版本中丢弃数据库列和相关的代码。例如,在版本 1 中部署新代码,这些代码不再使用某些数据库列,然后在版本 2 中才将不用的列移除。
- 不分片的数据库每周都会进行升级。在生产环境中我们使用脚本进行升级,在 QA 环境使用 Liquibase,后续会逐步在生产环境中也使用这个工具。
- 分片数据库使用自动化脚本来修改表结构。
- 分片数据库的迁移是个痛点,因为我们有 1 万 2 千多个分片,而且还在不断增长,一次升级没一个小时是搞不定的。
- 我们通过 schema 检查报告来确保在升级之后所有数据中心的数据库 schema 是一致的。
英文原文
Egnyte Architecture: Lessons Learned In Building And Scaling A Multi Petabyte Content Platform