发布于 2026-01-06 1 阅读
0

Billions of Emails Synced with Python

使用 Python 同步数十亿封电子邮件

开发邮件同步引擎很困难。Python 让这一切变得简单(或者说更容易)。

在 Nylas,我们对Python 的热爱毫不掩饰。我们的邮件同步引擎——负责为客户同步和处理超过 100 亿封邮件——完全是用 Python 构建的。

为什么?答案很简单:Python 标准、可靠,而且(对像我们这样的初创公司来说最重要的是)它很简洁。它是一个朴实无华的解决方案,使我们能够为全球开发者提供灵活稳定的 API。

在 2017 年的 PyBay 大会上,我发表了演讲,阐述了 Python 的“枯燥乏味”为何反而成为一种优势。对于构建复杂产品的公司而言,基于 Python 的解决方案能够为创新奠定最稳定的基础。

电子邮件很难用。

电子邮件协议出了名的不稳定,即使只是想实现与使用某种协议的 SaaS 应用的简单同步,也需要大量的文档。如果开发者希望他们的电子邮件实现能够兼容多种电子邮件标准,除了解析、编码、身份验证等问题之外,他们还需要考虑很多其他因素(例如,如何让 Exchange、Exchange ActiveSync 和 Gmail IMAP 协议协同工作)。潜在问题清单很长,而且几乎没有简单的解决方案。

我只想读封邮件而已!

这些复杂性仅仅是电子邮件这项已有50年历史的技术不可避免的包袱。标准会改变,新的、复杂的技术会不断涌现,但必须始终考虑遗留协议。

挑战

Nylas 最初启动时,我们必须考虑如何构建同步引擎 API。基本上有两种选择:1)存储最少的数据,并充当两个平台之间的转换层;2)镜像邮箱内容,并尽可能多地直接处理请求。

第一种方案成本更低,但会影响我们为依赖正常运行时间的组织提供可靠性服务的能力。因此,我们选择了第二种方案,尽管它成本更高。我们通过数据库分片和采用半单体架构来实现这一点。这种架构将我们的服务部署在不同的机器集群上,但它们共享相同的底层代码和模型,从而简化了集成。

架构:半单体应用

这种架构使我们能够更好地应对更大的挑战,例如邮件线程、标签和文件夹同步等等。最终成果:Nylas 同步引擎。Nylas 同步引擎是开源的,并提供 RESTful API,使开发人员能够将消息传递功能集成到他们的应用程序中。它包含超过 9 万行的 Python 代码,包括测试和迁移,并有助于管理包含协议、协议分支、解析、编码等在内的庞大生态系统。

在技​​术栈中,平庸反而是好事

为了构建我们的架构,我们必须使用像 Python 这样可扩展的编程语言及其众多库。例如,我们大量使用解析库flanker来提高电子邮件的送达率。此外,我们还使用了 Flask、Gevent、SQLAlchemy 和 pytest,以及其他工具,例如 HAproxy、nginx、gunicorn、MySQL、ProxySQL、Ansible、Redis 等等。就技术栈而言,我们似乎做出了一些相当平庸的选择。的确如此,而这正是我们有意为之。

我们选择使用看似平庸的工具,原因很简单:我们是一家小公司,能够投入到创新研发中的资源非常有限。事实上,我们部分设计理念源自丹·麦金利(Dan McKinley)的文章《选择平庸的技术》(Choose Boring Technology)(如果你还没读过,强烈推荐——非常精彩)。他在文中指出,每家公司在创新能力耗尽之前,都面临着被淘汰的风险。这一理念贯穿了Nylas的早期决策,并影响着我们核心的API理念,即让客户只需构建一个集成,而非多个。这促使我们选择那些久经考验、值得信赖的技术,无需我们操心,也让我们能够将精力投入到其他方面。

例如,我们使用 MySQL 来管理数据库。我们很早就决定选择 MySQL,因为我们对它非常熟悉,很多潜在的数据库管理员也都很熟悉它,而且我们需要把创新精力投入到其他方面。当然,依赖 MySQL 也意味着我们会经历很多成长的烦恼(参见我们关于“与 MySQL 一起成长”的博文),但我们通过巧妙地应用 ProxySQL、水平分片和其他技术克服了这些问题。

了解我们在 MySQL 方面的优势,以及如何利用 ProxySQL 和其他工具对其进行修改,有助于我们将数据库简化为更小、更易于管理的模块,同时几乎无需停机,也无需彻底重新设计数据库。事实上,我们利用 MySQL,借助 ProxySQL 和 SQLAlchemy 提供的强大功能,轻松记录和重放事务表中邮箱同步的所有更改。这为我们的同步、Webhook、流式 API 等功能提供了支持。

由于依赖项众多,我们使用封装在 Debian 包管理器中的 Python 虚拟环境来部署应用程序。具体来说,我们使用的工具名为dh-virtualenv,它使我们能够使用 Debian 的包管理器 dpkg 来部署依赖项,并在将工件推送到Amazon S3之前对其进行验证。正如 McQueen 指出的那样,一个简单的部署脚本最终看起来像这样:

scp my-package.deb remote-host.example.org:
ssh remote-host.example.org

# Run the next commands on remote-host.example.org
dpkg -i my-package.deb

/usr/share/python/myproject/bin/python
>>> import myproject # it works!
Enter fullscreen mode Exit fullscreen mode

有了这些工具,我们就能拥有一个可靠的技术栈,它无需重新发明轮子。它只是帮助我们同步和发送电子邮件,同时满足所有人对正常运行时间、可扩展性和稳定性的期望。

同步和检查

我们在所有服务器上都使用多核机器运行 Python。这意味着我们需要运行多个进程才能充分利用这些核心。为此,我们使用协程库gevent,在单个进程中同步大约 100 个账户。这大大节省了内存和操作系统调度资源。以下是一个进程的示例:

每个绿色方框都是一个 gevent greenlet。Greenlet 是一个功能强大的 Python 小工具,它允许我们创建精细的控制流程,例如,一个 greenlet 可以管理 Gmail 同步流程,还可以用多个 greenlet 管理回收站、日历、联系人同步等等。为了监控所有这些流程,我们使用了一个特殊的 greenlet,它在所有流程中运行,如果在设定的时间内没有运行,就会发送一个事件日志。当这种情况发生时,我们会运行一个采样分析器,对应用程序调用堆栈进行采样和记录,以确定应用程序卡住的位置,从而进行分析。在 Python 中,采样器的代码大致如下:

import collections
import signal

class Sampler(object):
   def __init__(self, interval=0.001):
     self.stack_counts = collections.defaultdict(int)
     self.interval = 0.001

    def _sample(self, signum, frame):
     stack = []
     while frame is not None:
         formatted_frame = '{}({})'.format(frame.f_code.co_name,
                                             frame.f_globals.get('__name__'))
         stack.append(formatted_frame)
         frame = frame.f_back

     formatted_stack = ';'.join(reversed(stack))
     self.stack_counts[formatted_stack] += 1
     signal.setitimer(signal.ITIMER_VIRTUAL, self.interval, 0)

    def start(self):
     signal.signal(signal.VTALRM, self._sample)
     signal.setitimer(signal.ITIMER_VIRTUAL, self.interval, 0)
Enter fullscreen mode Exit fullscreen mode

可以将这些样本输入到火焰图中,以显示进程正在做什么、CPU 的使用情况以及哪个 greenlet 阻碍了进程。

我们接下来的工作重点

但任何组织都知道,乐趣并不仅限于克服挑战。未来几年,我们计划加大对 Python 生态系统的投入。例如,随着代码复杂性的增加,我们正在考虑使用mypy来减轻管理负担。具体来说,我们使用 mypy作为代码检查工具进行类型检查。这是一个循序渐进的项目,但我们很高兴能够借助 mypy 降低代码复杂性的强大功能。

我们还将开始向 Python 3 迁移。最后,我们正在考虑将事务日志迁移到Kafka事件主干。这将使我们能够转向基于微服务的架构,无需每个服务都直接与数据库通信,从而获得更大的灵活性。

对我们来说,Python几乎渗透到我们所做的每一件事中,因为它简单易用、库丰富多样,而且能够很好地与服务器协同工作。对于充满好奇心的开发者来说,Python还有很多值得学习的地方,例如如何简化代码,或者如何为新应用奠定基础。而且,得益于其强大而活跃的社区,学习如何运用这些潜力也相当容易。正是这个社区让我们如此广泛地使用Python,也正是这个社区让我们期待看到它未来能带来什么,以及我们自己如何为其做出贡献。对我们而言,Python是标准的、可扩展的、久经考验的,而且最重要的是——对于任何初创公司来说,它很实用。我们迫不及待地想看看它未来的发展方向。

点击此处观看我的完整演讲:

本文最初发表于Nylas Engineering 博客

文章来源:https://dev.to/nylas/billions-of-emails-synced-with-python-61a