2016年6月,一个无响应的第三方域名系统(DNS)服务器导致旧版登录服务的中断,影响车手和驱动程序尝试访问UBER应用程序。虽然这个问题在几分钟内减轻了,但发现这一点发生了原因更具挑战性。作为优步对架构稳定和可靠的运输解决方案的一部分,我们的工程团队努力预防,回应和减轻妨碍无缝用户体验的中断。
在本文中,我们努力回答这个问题:第三方服务如何降低web服务的数据中心本地(甚至主机本地)连接?虽然这个登录服务已经被弃用,但是我们的经验让我们创建了一个新的解决方案来识别和防止这些类型的中断。继续读下去,了解一个无反应的人是如何反应的node.js.-dns服务器互动导致服务中断,步行通过请求处理的简要历史,并满足DNS拒绝,我们的开源解决方案防止无意否定(DOS)由DNS中断。
技术背景:DNS
大多数工程师员工dos byDNS会受到网络攻击,但一些运行时也容易受到资源耗尽的影响,因为DNS运行速度变慢。更具体地说,使用外部api会影响服务的关键内部调用。
下面,我们在Linux,Uber当前基础架构操作系统上提供一些关于域名分辨率的背景,以及Web服务器的线程模型,以上下文化我们的用例:
在Linux上使用DNS
在高级别,DNS负责将域名解析为机器可理解的IP地址。这些可以是互联网(如uber.com), network-local(例如,openwrt.lan.可能会解析到当前家庭网络中的路由器)或主机本地(如localhost.可以解决其中任何一个:: 1或127.0.0.1)。
解析外部域需要调用外部DNS服务器,该服务器可能需要要长得更长localhost.。但是,在Linux上用于解析的接口是相同的uber.com和主机:libc.呼叫getaddrinfo(3)。它的签名是:
Int GetAddrinfo(const char * node,const char *服务,
const struct addrinfo *提示,
struct addrinfo ** res);
只要可以从函数签名和手册中看到,呼叫是同步的:没有办法要求DNS“解决这个名称并在完成时返回给我。”
请求处理流程和线程
现在我们了解了DNS查询是如何在a中解析的libc.基于实施,让我们讨论三种类型的线程模型,以更好地掌握Node.js(我们构建的登录服务的运行时之间的交互方式和第三方服务导致中断。
CGI模型
在20世纪90年代初期,通过分叉子流程来处理传入的Web请求。通过处理动态内容(即,非静态内容)公共网关接口(CGI),如下图1所示:
该模型清楚地分离了应用程序代码(例如,在TCL.那Perl, 要么C)从web服务器代码(例如Apach.),两者之间的易于理解的界面。因此,该模型对开发人员具有吸引力。CGI工作得很好,因为较少人使用互联网,编程语言不需要一流的支持超文本传输协议(http),它为构建Web应用程序提供了更多语言选项。
然而,随着互联网的流行度,工程师意识到以快速速度开始和关闭流程是过于记忆和CPU密集的可持续性。为了节省硬件和电力资源,该行业必须开发更好的处理来电用户请求。
线程池(或1:m)模型
鉴于管理传入请求的昂贵部分是产卵过程,直观的解决方案是重用多个用户的进程。这意味着在任何给定时间,固定数量的进程等待处理请求。
虽然进程池是有效的,但大多数运行时都选择实现线程池。线程池是一个固定数量的线程,从开始到结束时处理用户请求,如图2所示,如下图2所示:
要服务请求,应用程序需要这样做读(阅读传入用户的查询),执行业务逻辑(如图2中的三个点所描绘),以及写将结果返回给用户。上面的图2突出显示了一个有三个用户和两个线程的调度示例,其中一个用户在整个请求过程中“拥有”线程。用一根线m客户一次,模型称为1:m。
现在我们了解线程池模型,我们实际如何配置它?CPU核心一次可以运行一个线程,因此它至少有一个N线程池中的线程,其中n是可用核心的数量。
1:M型号仍然是现代网络技术中的流行方法。但是,要挤出最后一个服务器的CPU周期,我们转向另一个请求处理方法:M:N模型。
M:n型号
要提供用户的请求,服务通常需要来自本地网络或磁盘的其他信息。例如,在Uber的前一个登录服务中,每个请求将新请求生成给用户或第三方服务。在此期间,处理请求的线程在其作用之前等待来自网络的回复。这是我们登录服务的情况;计算最小,大多数时间都花在等待其他服务中的回复。
由于CPU核心一次只能运行一个线程,因此它是常见的“超高订阅”线程。这样,正在等待外部回复的线程被删除(descheduled)CPU,并且仅安排正在进行工作的线程。在使用线程池的标准Web应用程序上,花了很大一部分时间等待外部请求。为了保持CPU忙,需要比有核心更大数量的线程。
上下文切换 - 换句话说,从CPU内核中脱节线程并调度新的一个 - 可能是昂贵的。(有关此主题的更多信息,请退房“制作上下文切换需要多长时间?”)。在规模上,上下文切换是如此的资源密集型,以至于工程师们发明了一种完全不同的方法:M:N调度,如下图所示:
如图3所示,线程需要读处理用户的请求,然后执行一些处理(在上图中用窄条纹表示),最后写。使用M:n计划,线程为每个用户同时为每个用户提供一点工作,而不是一次处理一个。
在M:n线程模型中,线程处理当时输入易于获得的用户的请求。一旦从网络或磁盘获取第二(第三,第四,第四等)用户的信息,给定的用户的输入将被调度回线程并继续直到另一个外部呼叫或完成(写)。
在这种情况下,可能有m处理短时间突发用户计算的线程N计划的核心,这些核心,贷款到其名称M:n。由于CPU内核未在外部呼叫上被阻塞,因此它很常见m = n。
由于用户输入没有阻止线程,CPU内核始终正在进行工作,但由于线程没有许多线程,因此上下文切换很少见。
对于Uber的登录服务,M:n型号很好地工作,直到我们开始在Node.js中遇到DNS和User M:n之间的负相互作用。
在M:n安排中的DNS
M:n型号仅在应用程序代码清楚地说明“我现在正在等待网络回复时,才会免费击落我。”在幕后,这意味着它选择(2)(或等效)在多个套接字上。然而,异步I/O和DNS调用通常不会表现出快速和可靠的行为。
一个时间密集的同步呼叫将阻止线程并表现出上面的1:m线程池的问题:暂停线程等待从未工作的外部实体的回复。
为避免这种情况,可以将同步系统调用分配给单独的线程池:
在这个场景中,在专用线程上执行的同步系统调用不再阻塞主要线程。通常DNS和I/O不会像调用外部服务那样频繁地发生,因此上下文切换的每个请求成本降低了。
在许多方面,M:n与专用线程池的调度为优步登录服务提供了理想的解决方案,因为主线程始终正常工作而不是竞争CPU。相比之下,用于昂贵的同步呼叫的线程池卸载主线程上的慢速操作。
通过了解Linux,线程池,M:n调度和同步系统调用的DNS解析,现在讨论该技术组合如何导致登录服务中断。
分析我们的登录服务中断
2016年夏天,一个反应迟钝的第三方DNS服务器造成一个超级登录服务中断,影响了一些乘客和司机试图登录到应用程序。在很短的时间内,大部分我们的用户无法登录与外部提供者或手动输入自己的用户名和密码组合。
我们的登录服务使用标准Node.js HTTP客户端与其他服务通信。要与同一数据中心内的其他服务进行通信,登录服务连接到路由器代理haproxy.上localhost.(也称为a双轮马车模式)。然后,路由器代理将请求提交给适当的依赖项(在我们的情况下,密码服务)。
服务架构由以下部分组成:
- 一种登录服务(在http)负责接受用户名/密码组合并返回会话令牌。
- 一种密码服务它确定这对凭据是否正确。
- 一种第三方提供商它启用了额外的登录方法。
这三块的结合导致了我们的停电。我们概述了它的进展,下面是:
- DNS查询第三方提供商开始花费比平时更长。最初,我们假设这是因为超出了优步控制的第三方DNS服务器。
- 然后,登录服务不仅无法达到这些提供商,还无法访问内部密码服务以及网络内的所有服务,即使localhost.。
- 在同一时间,我们确认了我们的一个提供者的中断。对其端点的DNS查询确认DNS响应时间从数十毫秒跳跃到几十秒。
在中断期间,我们注意到其中一个数据中心的登录没有受到影响,并且我们能够在几分钟内通过将客户流量转移到一个不受影响的数据中心来缓解中断。然而,我们仍然不明白为什么一开始会发生宕机。
根本原因分析
解决localhost to :: 1(需要连接到本地sidecar)包括调用同步getaddrinfo(3)。此操作在专用线程池中完成(默认为尺寸4.在node.js中)。我们发现,这些长DNS反应使线程池不可能快速服务localhost.至:: 1转换。
结果,我们的DNS查询都没有(即使是localhost.),这意味着我们的登录服务无法与本地Sidecar通信以测试用户名和密码组合,也不调用其他提供者。从UBER应用程序的角度来看,没有任何登录方法工作,用户无法访问该应用程序。
To prevent this from happening in the future at Uber and elsewhere, we created an open source solution to test whether or not a service’s language is affected by this type of DNS interaction, as well as put together a series of precautionary measures for avoiding this type of outage, both of which are introduced in our next section.
我们的解决方案:DNS拒绝
在进行对DNS调度如何引起这一中断雷竞技是骗人的的研究之后,我们决定通过DNS拒绝向开源社区提供一些调查结果,我们的解决方案来测试您的服务的语言是否易于通过DNS中断对DOS敏感。
为了测试你的语言是否受到影响,编写一个程序,遵循以下简单的步骤:
- 呼叫http:// localhost:8080。这个电话应该始终工作;未能这样做意味着测试中存在错误。
- 呼叫http://example.orgn次并行;不要等待结果。N通常略高于默认线程池大小。
- 等待几秒钟以确保计划所有呼叫。
- 呼叫http:// localhost:8080。如果应用程序没有漏洞,则此调用将成功。
脚本检查次数http:// localhost:8080叫做:
- 0:设置有一个错误,因为脚本应该至少成功一次。
- 应用程序是脆弱的,换句话说,第一次调用成功而第二次调用失败。
- 2:应用程序不存在漏洞。
作为本练习的一部分,我们测试了服务erlang.那去,node.js,和龙卷风若要确定m:n调度是否“安全”(意思,它不会导致DNS可靠性问题)之间的环境:
| 名称 | 评论 | 安全 |
| erlang-httpc | Erlang 20带有INETS HTTPC | uns |
| golang-http | golang 1.9与stdlib的'net / http' | 安全 |
| nodejs-http. | 节点8.5与stdlib的“http” | uns |
| Python3-龙卷风 | Python 3.5.3和Tornado 4.4.3 | uns |
使用我们的新工具,您需要的只是运行一个简单的测试,以确定您的运行时是否受M:N调度的影响。
除了DNS的使用拒绝,还有一些其他策略可以采取措施来避免潜在的漏洞语言-DNS交互:
避免getaddrinfo()系统完全呼叫
防止阻止运行时间getaddrinfo(3),可以通过完全避免呼叫来解决DNS地址。例如,节点DNS.是用纯粹写的DNS解析器javascript.,避免了这个问题。注意,非libc实现可能会返回不同于本机调用的结果。
用IP地址替换众所周知的域
保持服务与本地系统的连接(例如,通过侧车打开)localhost.),人们可以取代localhost.借:: 1要么127.0.0.1.。这种变化可以缓解资源疲惫的症状;虽然将阻止取决于DNS的外部传出流量,但预配置的内部端点将继续运行。
尽管如此,这不是解决问题的完整解决方案;它只有助于当拨出呼叫的子集(在我们的情况下,在同一数据中心内)不依赖于DNS。
使用非受影响的语言
在我们的验尸研究期间,我们用M:N调度测试了一雷竞技是骗人的些运行时,并验证了其中一些不受影响。在重写现有服务到未受影响的运行时可能是一个沉重的要求防止此问题,在开发将与第三方服务交往的新系统之前,这可能是值得考虑的。
其他外卖
虽然我们的事故缓解快速而成功,但数据中心故障转移被认为是最后的手段。在进行中断分析之后,我们添加了一种更轻量级的缓解技术,以防止再次发生这种情况:使用运行时配置标志禁用依赖于外部服务的功能。只需点击几下,我们就可以禁用任何第三方供应商,并为其他用户减轻问题,而无需将所有流量转移到另一个数据中心。
下一步
通过DNS中断发现DNS中断的DNS的根本原因使我们能够创建一个更稳定的环境,以便于在Uber应用程序上提供更好的体验,无论用户如何选择访问它们。我们希望您找到我们的测试(和建议),用于处理对您自己的项目有用的这种停电!
如果使用M:N的运行时,我们建议您应用我们的测试以确定您的运行时是否受到影响并提交拉动请求我们的存储库因此,其他人也能从中受益。
如果对您的规模吸引力进行故障排除,请考虑申请职务在我们的团队。
在停机的时候,Motiejus木菠萝š泰是一个软件工程师在超级市场团队,组织,维护Ringpop。目前,他是位于立陶宛维尔纽斯的优步基金会平台团队的一名软件工程师。
照片标题信用,“GentooPenguin捍卫小鸡从Skua,”由Conor Myhrvold,海狮岛,福克兰群岛,2007年1月。






