原文:Stack Overflow: The Architecture - 2016 Edition
作者:Nick Craver
译者:孙薇
本篇是Stack Overflow架构系列文中的第一篇,请继续关注本系列。
想要了解Stack Overflow“做”了哪些工作,我们从日均更新开始看起。先做一下数据比对,具体请看下表:
类目 | 2013年11月 | 2016年2月9日 | 数据差异 |
---|---|---|---|
发往负载均衡器的HTTP请求 | 148,084,883 | 209,420,973 | +61,336,090 |
其中页面加载次数 | 36,095,312 | 66,294,789 | +30,199,477 |
发送的HTTP流量(bytes) | 833,992,982,627 | 1,240,266,346,053 | +406,273,363,426 |
收到的总数据量(bytes) | 286,574,644,032 | 569,449,470,023 | +282,874,825,991 |
发出的总数据量(bytes) | 1,125,992,557,312 | 3,084,303,599,266 | +1,958,311,041,954 |
SQL查询(仅从HTTP发出的请求) | 334,572,103 | 504,816,843 | +170,244,740 |
Redis hits | 412,865,051 | 5,831,683,114 | +5,418,818,063 |
Elastic搜索 | N/A | 17,158,874 | N/A |
标签引擎请求 | 3,603,418 | 3,661,134 | +57,716 |
运行SQL查询消耗时长 | 558,224,585毫秒(155小时) | 607,073,066毫秒(168小时) | +48,848,481 |
Redis hits消耗时长 | 99,346,916毫秒(27小时) | 10,396,073毫秒(2.8小时) | -88,950,843 |
标签引擎请求消耗时长 | 132,384,059毫秒(36小时) | 147,018,571毫秒(40.8小时) | +14,634,512 |
处理ASP.Net的时长 | 2,728,177,045毫秒(757小时) | 1,609,944,301毫秒(447小时) | -1,118,232,744 |
另外,渲染49180275个问题页面的平均耗时降低到22.71毫秒,减少了5.29毫秒,在ASP.Net中消耗了19.12毫秒;渲染6370076个主页的平均耗时(毫秒)降低到11.80毫秒,减少了53.2毫秒,在ASP.Net中消耗了8.81毫秒。
对比2013年的数据(757小时),尽管日请求数增加了6100万个,ASP.Net的处理时长反而大幅缩短。这是因为在2015年初,我们对硬件做了升级,同时在应用内做了许多性能调整。请不要忘记:性能也属于功能之一。本系列文中会讲到更多硬件方面的细节,在下篇文中,将会列举运行网站的所有服务器硬件的详细规格,请继续关注。
那么过去两年间有哪些改变呢?除了替换掉一些服务器和网络设备之外,其实并没有太大变化。下面列举了运行网站所需的顶级硬件设备(注意与2013年时的差别):
- 4台微软SQL服务器(其中两台采用新硬件)
- 11台IIS网络服务器(新硬件)
- 2台Redis服务器(新硬件)
- 3台标签引擎服务器(其中两台使用新硬件)
- 3台Elasticsearch服务器(与之前的相同)
- 4台HAProxy负载均衡器(增加两台以支持CloudFlare)
- 2套网络(每个包括1个Nexus 5596核心+2232TM Fabric Extenders,全部升级到10Gbps带宽)
- 2个Fortinet 800C的防火墙(代替Cisco 5525-X ASAs)
- 2台Cisco ASR-1001路由器(代替Cisco 3945 路由器)
- 2台Cisco ASR-1001-x路由器(新设备!)
运行Stack Overflow需要什么设备呢?从2013年以来变化不怎么大,不过由于优化(上面提到过)及硬件更新,现在的系统需求可以削减到一台网络服务器。我们无意间测试过这样的配置,并且成功了几次。需要澄清的是:这样的配置仅是能用而已,但不是优秀的配置。不过这种配置也很有趣就是了。
现在关于扩展的概念我们有了一些基线数据,下面将会具体讲述那些花哨页面的做法。几乎没有系统可以完全单独存在,我们的系统也不例外。在构建部分时,如果缺少对整体的适用性和大局观考量,这样的架构决策没有太大意义。这就是我们的目标——掌控全局。在很多后续文中会对特定领域做深入介绍。本文仅对重点硬件做逻辑性概览,下篇将会讲述硬件设施的细节。
关于目前的硬件设施外观,下面是笔者在2015年2月升级期间所拍摄的机架A的照片(机架B与它一模一样):
如果有兴趣了解更多,可以点击这里查看相册,里面包含了256张后续拍摄的照片(没错,这个数字是故意的)。现在,我们继续深入布局。下图列出了当前使用中的主系统逻辑概览:
基本原则
这是一些全局性的原则,本文不会再逐条赘述:
- 所有东西都有冗余备份。
- 所有服务器与网络设施都有至少2x10Gbps的带宽。
- 所有服务器都有双重电源供给:由2台发电机与2台utility feeds所组成2部不间断供电机组。
- 所有服务器都在机架A与机架B之间有冗余备份。
- 所有服务器与服务都留有存放在科罗拉多州数据中心的双倍冗余备份,尽管我们主要讨论的是位于纽约的服务器与服务。
- 所有东西都有冗余备份(重要的事情说两遍)。
互联网
首先得找到我们的网站,这就得靠DNS。为了满足快速定位的需求,我们使用了CloudFlare的DNS服务(目前阶段),因为他们的DNS服务器遍布世界各地。我们通过API升级自己的DNS记录,而他们负责“托管”DNS。不过出于防人之心不可无这样的深层信任问题,我们仍保留了自己的DNS服务器。如果出现灾难性事故(也许是GPL、Punyon——Stack Overflow的一名数据开发人员或者缓存造成的),人们想要借编程打发时间,就会切换到自己的DNS服务器上。
找到我们的秘密藏身所之后,就可以通过四家ISPs(位于纽约的Level 3、Zayo、Cogent、Lightower )中的某家,再经由四台顶尖路由器中的某台,通过HTTP来访问我们了。我们使用边界网关协议(BGP,非常标准的协议)来匹配ISP服务,以便对流量进行管理,并提供几条访问最便捷的道路。ASR-1001和ASR-1001-X分别是两套路由,采用双主机的方式(active/active fashion)为2个ISP提供服务,因此这里也有冗余备份。尽管两者同样具有10Gbps的物理带宽,但外部流量与连接着负载均衡器的内部VLAN是独立分开的。流量经由路由器,进入到负载均衡器。
这里要提一下,我们的两台数据中心通过10Gbps带宽的MPLS网络相连,不过跟网络服务所使用的网络没有直接关联。这个网络是用来执行数据备份,以及在突发情况下的快速备份的。有人会说,这不是冗余备份!技术上来说没错,非常正确,这属于单点故障。不过等一下!在ISP那里,我们还维护着另外两台故障转移可用的OSPF路由呢(根据成本来排序,MPLS第一,这两台分列二三)。前面提到的路由组都与科罗拉多州的相应路由组相连,在需要故障转移时执行流量负载均衡的任务。我们可以在这两台路由组之间搭建互联,这样就有四条通路了,不过不管怎样,我们继续往下说。
负载均衡(HAProxy)
负载均衡使用了CentOS 7(我们最喜欢的Linux),通过HAProxy 1.5.15执行。TLS(SSL)的流量也被导入HAProxy。我们热切期盼HAProxy 1.7早日发布,就能支持HTTP/2了。
其他服务器拥有双路10Gbps带宽的LACP网络连接,与之不同,每台负载均衡器拥有两组10Gbps的带宽:一组服务于内网,一组服务于DMZ。这些机组有着64GB甚至更大的内存,以便更高效地处理SSL协议问题。如果能将更多TLS会话缓存在内存中以便重复使用的话,在与同一台客户端多次相连时就可少些重复运算了。这也意味着我们可以更快速、更廉价的重启会话。由于内存非常廉价,很容易作出这样的选择。
负载均衡器自身配置非常简单。我们监听着各种IP的不同网站(大多是出于证书和DNS管理考量),再根据主机名(主要)将其导入各种后端。我们所做的事情中,仅有两点值得注意,一是速率限制,二是捕获一些(发自我们的web层)的主机名并录入HAProxy syslog信息,这样我们就能记录单独各个请求的性能指标了,稍后会详细讨论。
Web层(IIS 8.5, ASP.Net MVC 5.2.3,.Net 4.6.1)
负载均衡器负责将流量分配到9台服务器(01-09,“主服务器”)与2台网络服务器(10-11,“开发/meta”服务器,staging环境)中。主服务器负责处理Stack Overflow、Careers以及所有的Stack Exchange站点事宜,除了下面这两个站点:meta.stackoverflow.com和meta.stackexchange.com——这两个站点相关的事宜会放在后两台服务器上。主要的Q&A应用自身是多租户的,也就是说单个应用为所有来自Q&A站点的请求提供服务。换句话说:我们可以在单个服务器的单个应用池中,运行整个Q&A网络。其他应用,比如Careers、API v2、Mobile API等都是独立分开的。下面列出了IIS中的主要与开发层面:
下面是在Opserver中,Stack Overflow在web层面中的分布模式(我们的内部监控面板):
下面是这些web服务器的资源利用率情况:
后续文章会进一步讲述这样分配的原因,不过主要是因为:滚动生成、回旋余地、冗余备份。
服务层(IIS, ASP.Net MVC 5.2.3,.Net 4.6.1与HTTP.SYS)
在这些web服务器之后,还有非常类似的“服务层”。同样是在Win2012R2上运行的IIS 8.5。这一层面运行着内部服务,对生产web层和其他内部系统提供支持。主要包括两个重要服务:基于http.sys(非IIS的)、运行在标签引擎上的“Stack服务器”;还有Providence API(基于IIS)。有趣的事实:我必须对这两个进程分别设置关联性,以便连接不同的socket,因为每隔两分钟刷新问题列表时,Stack服务器会频繁访问L2和L3的缓存。
标签引擎和后端API依靠这些服务器完成大量工作,因此我们需要冗余备份,但不是9重冗余备份。例如,从数据库每隔n分钟(目前是2分钟)加载一次所有文章及其标签,所消耗成本并不算低。我们不想在web层面重复加载9次;3次就够了,已经有足够的安全性保障。在硬件端,我们对这些服务器执行了不同的配置,根据标签引擎与elastic索引任务的不同,针对其计算负载特性提供更佳的优化(同样在这一层中运行)。“标签引擎”本身就是一个非常复杂的话题,后面会有专门的文章来说明。基本就是这样:在用户访问/questions/tagged/java时,系统就会访问标签引擎以搜索匹配的问题。除了/search,所有的标签匹配都是由它来处理的,因此新导航等任务都会使用这项服务来获取数据。
缓存& 发布/订阅 (Redis)
我们使用Redis来完成一些任务,它用起来非常可靠。尽管每月的ops达到1600亿,所有实例的CPU占用都不超过2%,通常甚至更低:
我们在Redis中有一个L1/L2缓存系统。“L1”是存在于web服务器或者当前其他应用中的HTTP缓存,“L2”位于Redis中,负责取回数据。我们的数据都是借助Marc Gravell编写的protobuf-dot-net,以Protobuf格式存储的。对于客户端,我们使用了StackExchange.Redis,这是一个内部开发的开源系统。如果某个web服务器没能在L1和L2中找到缓存,就会通过数据库查询、API调用等办法从来源抓取数据,并将结果保存在本地缓存和Redis上。其他想要获取数据的服务器在L1缓存中可能找不到相应数据,不过在L2和Redis中也许有所发现,就无需再查询数据库或调用API了。
我们还运行着很多Q&A站点,每个站点都有自己的L1/L2缓存:在L1中通过key前缀,在L2和Redis中通过数据库ID来进行搜索。后续文章中会有进一步探讨。
这两台主要的Redis服务器(主/伺各一)运行着所有的站点实例,另外还有两台服务器专门用于机器学习实例(由于内存的问题)。这两台服务器用于在主页推荐问题、与任务进行更好的匹配等等,其架设的平台名为Providence,点击这里查看Kevin Montrose的相关描述文章。
主要的Redis服务器有256GB内存(使用约90GB),而Providence服务器有384GB内存(使用约125GB)。
Redis不止提供缓存服务,还用于发布和订阅机制——一个服务器发布某条信息后,其他订阅者(包括位于Redis伺服器上的下游客户端)就会收到这条信息。在一台web服务器作出删除操作时,我们使用这一机制来清理其他服务器上的L1缓存,保持一致性。不过还有更大的用处,就是:websockets。
Websockets (NetGain)
我们使用websockets向用户实时发布更新,比如顶栏中的通知信息、投票数、新导航数、新的回答与评论以及其他一些东西。
socket服务器自身使用了运行在web层的raw sockets。这是一个在我们开源数据库顶层运行的一个非常轻量的应用:StackExchange.NetGain。峰值时达到大约50万的并发websocket连接,真是大大堆的浏览器。有趣的是:这些浏览器中有些已经打开超过18个月了,具体原因尚不清楚。也许得有人去看看那些开发者是不是还活着了。下图是本周并发websocket的模式:
为什么是websockets呢?因为它们非常高效,按照我们这种规模,效率远胜于轮询制度。通过这种方式,非常简单就能利用更少的资源推送更多的数据,对用户来说也更快捷。的确还有问题存在,包括临时端口与负载均衡器上的文件句柄耗尽,这些有趣的问题都是我们在后续文章中会讨论到的。
Search (Elasticsearch)
剧透:这里令人兴奋的内容并不算多。web层使用非常轻量级的高性能StackExchange.Elastic客户端,通过Elasticsearch 1.4进行出色的vanilla搜索。 比较特别的是,我们不打算将这个客户端开源,因为它仅仅暴露了我们所使用的API子集很少的一部分。笔者坚信,将其发布出来弊大于利,只会让开发者感到困惑。我们使用elastic来执行/search任务,计算相关的问题,并在用户提问时给出建议。
每个Elastic集群(每个数据中心都有一个)都有3个节点,每个网站都有自己的索引。Careers还有额外的几个索引。在我们配置elastic时,与标准配置略有不同的是:我们的三台服务器集群比平均值略高(全SSD存储,192GB内存,双通道10Gbps带宽)。
在托管着标签引擎的Stack服务器上有同样的应用领域,同样在Elasticsearch中持续执行索引。这里我们用了些简单的技巧,比如将在SQL服务器上(数据源)的ROWVERSION与Elastic中的“最后位置”文档相比较。由于非常类似序列,在最后一次访问后,如果发生变更,我们就可以简单地抓取任何内容并进行索引。
我们使用Elasticsearch而不是SQL全文搜索之类的主要原因在于:前者拥有更佳的可扩展性与性价比。相对来说,如今SQL CPU非常昂贵,Elastic更为便宜,而且功能也更多。为什么不用Solr呢?我们想要在整个网络中执行搜索(一次很多索引),而在作出选择时Solr对这项功能还不支持。而不用2.x的原因在于,主要“类型”的变更代表着我们需要升级,将所有内容重新编制索引。作出这样的变更与迁移计划时间不够。
Databases (SQL Server)
我们没有启用SQL服务器作为单独的可信数据来源。所有Elastic和Redis上的数据都来自SQL服务器。我们运行着两台SQL服务器集群,配置了AlwaysOn可用性组。这些集群在纽约每个都有一个主服务器(几乎承载所有负载)和一个备份服务器。此外,它们各自在科罗拉多州(我们的DR数据中心)还拥有1个备份服务器。所有备份都是异步执行的。
第一个集群是一组Dell R720xd服务器,每个都有384GB内存,4TB PCIe SSD空间和2x12的内核。托管着Stack Overflow、网站(暂且用这个说法,稍后会解释)、PRIZM以及 Mobile数据库。
第二个集群是一组Dell R730xd服务器,每个都有768GB内存,6TB PCIe SSD空间和2x8个内核。这个集群运行着其他东西,包括Careers、Open ID、聊天、异常日志以及所有其他Q&A站点(比如Super User、Server Fault等)。
我们希望数据库层持续较低的CPU占用量,不过实际上由于我们正在解决的一些计划缓存问题,现在还是有些高的。截至目前,NY-SQL02和04都是主服务器,01和03都是副本服务器(由于一些SSD更新,发文日刚刚重启)。下面是过去24小时的图示:
我们使用SQL的方式非常简单,简单的话速度就快,尽管某些查询非常频繁,我们与SQL本身的互动还是相当vanilla式的。我们有一些遗留的Linq2Sql,不过所有的新开发都在使用Dapper,这是我们的开源Micro-ORM,使用了POCOs。换句话说:Stack Overflow在数据库中只有一个存储过程,我们计划将剩余的部分也换成代码。
库
现在讨论些更有直接帮助的内容吧。上面提到了一些,下面是一张列表,列出了很多我们维护的开源.Net数据库。由于没有太多核心商业数据,我们得以将其开源,以帮助全世界的开发者。希望对大家有用:
- Dapper (.Net Core):用于ADO.Net的高性能Micro-ORM;
- StackExchange.Redis:高性能的Redis客户端;
- MiniProfiler:轻量级分析工具,我们在每个页面上都有运行,也支持Ruby、Go和Node;
- Exceptional:故障日志记录工具,适用于SQL、JSON、MySQL等;
- Jil:高性能的JSON序列化/反序列化工具;
- Sigil:.Net CIL 生成助手,适用于C#速度不够快时;
- NetGain:高性能的websocket服务器;
- Opserver:监控面板,可以直接在大多数系统中执行轮询,并从Orion、Bosun或WMI中提取信息;
- Bosun:以Go语言写成的后台监控系统。
系列文的下一篇将会详细讨论目前运行代码的硬件配置,之后会按照列表继续推出文章,请保持关注。
(责编/钱曙光,关注架构和算法领域,寻求报道或者投稿请发邮件qianshg@csdn.net,交流探讨可加微信qshuguang2008,备注姓名+公司+职位)
「CSDN 高级架构师群」,内有诸多知名互联网公司的大牛架构师,欢迎架构师加微信qshuguang2008入群,备注姓名+公司+职位。