
这是该系列的下一篇文章,描述我们如何提高Citymobil的服务可用性(您可以在此处阅读之前的部分:
第1 部分 ,
第2 部分 ,
第3部分 )。 在其他部分,我将详细讨论事故和停机。
1.发布不良:数据库过载
让我从这种中断的具体示例开始。 我们进行了优化:在SQL查询中添加了USE INDEX; 在测试以及生产过程中,它加快了简短的查询,但是较长的查询却变慢了。 仅在生产中才注意到长时间的查询速度下降。 结果,许多长时间的并行查询导致数据库宕机了一个小时。 我们彻底研究了USE INDEX的工作方式; 我们在“注意事项”文件中对此进行了描述,并警告工程师不要误用。 我们还分析了查询,并意识到该查询主要检索历史数据,因此可以在单独的副本上运行以获取历史请求。 即使此副本由于过载而关闭,业务也将继续运行。
之后,我们继续对相同的问题之以鼻,在某个时候,我们决定解决此问题。 我们仔细研究了代码,并移走了所有可能的查询,而又不会损害我们对副本的服务。 副本本身根据其关键程度进行了划分,因此它们都不会失败并停止服务。 结果,我们提出了具有以下数据库的体系结构:
- 主数据库-用于对数据新鲜度超级敏感的写操作和查询;
- 生产副本-用于对数据新鲜度不太敏感的短查询;
- 价格飙升系数的副本。 这些副本可以落后30-60秒; 这不是至关重要的,因为系数不会经常更改,并且如果此副本出现故障,服务将不会停止工作; 价格不能完全满足供需平衡;
- 用于操作设置和呼叫中心的副本。 如果崩溃,则业务将继续运行,但没有客户和驱动程序的支持,我们将无法临时更改某些设置;
- 临时分析和仪表板的许多副本;
- MPP(大规模并行处理)数据库,用于一些大规模分析,并具有对历史数据的完整切片。
这种体系结构为我们提供了广阔的增长空间,并减少了由于非最佳SQL查询而导致的崩溃数量。 但这还远非完美。 我们计划实施分片,以便我们可以扩展更新和删除操作,并对数据新鲜度查询超级敏感。 MySQL的安全裕度不是无限的。 我们将很快需要一些内存数据库(例如Tarantool)形式的重型火炮。 我肯定会在下一篇文章中谈论它。
在处理非最佳代码和查询时,我们了解以下内容:任何非最佳性都应在释放之前而不是之后消除。 这样可以减少停机的风险和工程团队进行优化的努力。 如果已经在代码之上部署了一些新版本,则优化起来就困难得多。 因此,我们引入了强制性代码审查以进行优化。 它是由我们经验最丰富的工程师,我们的精英力量执行的。
我们还开始收集适用于我们在Do和Dont中的最佳代码优化方法。 它们在下面列出。 拜托,不要将这些做法当作不可否认的真理,也不要盲目地复制它们。 每种方法仅对某些特定情况和特定业务有意义。 这仅是一个阐明细节的示例:
- 如果SQL查询不依赖于用户(例如,具有最大票价和喘振系数的驾驶员需求图-该图对于驾驶员应用程序的任何用户都是相同的),则此查询必须在具有以下内容的cron脚本中执行一些特定的频率(在这种情况下,一分钟就足够了)。 结果必须写入应该在生产代码中使用的缓存(Memcached或Redis)。
- 如果SQL查询处理的数据对于业务而言并不是很关键,那么延迟必须放在具有一定TTL(例如30秒)的缓存中,然后由生产代码从缓存中读取该结果。
- 如果在特定的服务器方法实现中(使用PHP或另一种服务器端语言)决定进行SQL查询,则需要确保所需的数据没有随其他SQL查询一起到达或即将到达您的代码如下。
- 与上述相同,将请求发送到缓存。 缓存速度很快,但仍然是数据库。 因此它也可以超载。 一个常见的错误是您认为缓存是一种普通的内存变量,并将其用作变量。 但是访问它涉及网络往返,并为Redis或Memcached产生了工作负载。 因此,如果数据已经从缓存中获取,那么不仅要从缓存中获取已经获取的内容。
- 如果在Web请求处理期间(再次使用PHP或任何其他语言)需要调用函数,则需要确保在其中不进行额外的SQL查询或缓存访问。 如果不可避免要进行此类函数调用,则必须确保不能对其进行修改,或者其逻辑没有被破坏,以防止不必要的数据库/缓存查询。
- 如果必须执行SQL查询,则必须绝对确保不能将所需的字段添加到代码中上方或下方的现有查询中。
2.不良的手动操作
这些事故的示例:
- 导致数据库超载或导致副本延迟的错误ALTER;
- 错误的DROP(例如,我们遇到了MySQL中的一个错误,该错误在删除表时阻止了数据库);
- 对主控器的繁重查询,是错误地手动进行的;
- 即使在真正的工作负载下仍在配置Web服务器,但我们认为它无法运行。
为了最大程度地减少由于这些原因造成的停机,我们必须在每次事故发生时调查一次事故的性质。 我们还没有找到一个通用规则。 再次,让我们看一些具体的例子。 喘振系数(在高需求的时间和地点乘出租车的费用乘以它们)在某个时候停止工作。 原因是在数据库副本服务器上运行了一个python脚本,该脚本从其中获取了用于计算系数的数据,并且该脚本用尽了所有内存,而副本服务器崩溃了。 该脚本已经运行了一段时间; 为了方便起见,它在副本上正确运行。 通过重新启动脚本解决了问题。 得出以下结论:不要在数据库服务器上运行外来脚本(它是用“做”和“不要”编写的;否则,将是空洞的!),监视数据库服务器上的内存使用情况并通过SMS发出警报如果该服务器即将用完内存。
总是得出结论,而不是在“看到问题,解决问题,忘记它”这种情况时感到很自在。 只有得出结论,才能提供优质的服务。 除此之外,SMS警报至关重要-它们可以提高服务质量水平,不会让它下降并允许我们提高其可靠性。 就像一个登山者一样,他到达一个稳定的位置,然后将自己拉到另一个稳定的位置,但是这次只是更高。
监视和警报是不可见的,但是它们就像铁钩刺入未知的岩石一样,阻止我们跌落到我们不断增长的服务水平协议之下。
3.复活节彩蛋
我们所谓的“复活节彩蛋”-是一个延迟行动的地雷,尽管它已经存在了一段时间,但我们尚未绊倒。 在本文之外,该术语用于故意创建的未记录功能。 在我们的案例中,它根本不是功能,而是一个像定时炸弹一样起作用的错误,它似乎是某些精心设计的活动的副作用。
例如:
- 32位
auto_increment
溢出;
- 高工作量触发代码/配置的非最佳性;
- 由于使用新模式或工作量增加而导致的非最佳查询,导致复制副本延迟;
- 由新工作负载模式引起的对主服务器的非最佳UPDATE操作延迟了副本,并延迟了复制。
另一种流行的复活节彩蛋是非最佳代码。 更具体地说-非最佳SQL查询。 该表过去很小,工作量也很轻-查询工作得很好。 随着时间表的线性增长和时间表的线性工作负载的增加,数据库管理系统的资源消耗成倍增长。 通常,这会导致严重的负面影响:就像以前一切正常,然后突然-糟糕!
罕见方案-错误和复活节彩蛋的结合。 带有错误的发行版导致数据库表的扩大并增加了特定种类的表行的数量,而已经存在的复活节彩蛋由于对该扩展表的查询变慢而导致数据库超载。
但是,我们曾经有一些与工作量无关的复活节彩蛋。 例如,MYSQL中的32位
auto_increment
字段。 在超过20亿行之后,插入失败。 因此,在现代世界中,我们必须仅使用64位
auto_increment
字段。 我们很好地学习了这一课。
如何处理复活节彩蛋? 答案听起来很简单:a)搜索旧鸡蛋,b)不允许出现新鸡蛋。 我们正在努力做到这两者。 搜索旧鸡蛋与我们不断进行的代码优化紧密相连。 我们任命了两名经验最丰富的工程师来几乎全职进行优化。 他们在slow.log中找到使用数据库资源最多的查询。 他们优化了这些查询以及它们周围的代码。 通过测试上述传感工程师执行的每次提交的最佳性,我们降低了新蛋出现的可能性。 他们的任务是指出影响性能的错误,提出改进方法并将这些知识传递给其他工程师的方法。
在某个时候,在找到另一个复活节彩蛋后,我们意识到寻找慢速查询是一件好事,但是我们也应该寻找看起来很慢但工作迅速的查询。 这些是接下来的竞争者,以防万一另一桌爆炸式增长而使一切崩溃。 这是一个愚蠢但显而易见的示例,它是一个查询,它完全扫描10行的表,而根本不使用索引。 暂时可以快速运行。 但是,当表足够大时,查询将关闭数据库。 那就是复活节彩蛋。
4.外部原因
这些是我们似乎无法很好控制的原因。 换句话说,这些是只能缓解但不能消除的原因。 例如:
- 地图服务提供商限制我们的请求。 可以通过服务使用控制,遵守特定工作负载级别,预先计划工作负载增加以及购买服务扩展来缓解这种情况。 但是,我们离不开地图。
- 数据中心的网络故障。 可以通过将服务副本放置在备份数据中心中来缓解这种情况。 但是,我们离不开物理或云数据中心。
- 付款服务下降。 可以通过付款服务备份来缓解这种情况。 但是,我们离不开付款。
- DDoS保护服务错误地阻止了流量。 可以通过默认情况下关闭DDoS保护服务(仅在发生DDoS攻击时将其打开)来缓解此问题。 但是,我们离不开DDoS保护。
由于即使减轻外部原因也是一项长期而昂贵的工作,因此我们开始收集由外部原因导致的事故的统计数据,并等待关键的质量累积。 我们没有定义临界质量的方法。 这全都与直觉有关。 例如,如果我们由于DDoS保护服务问题而完全瘫痪了五次,那么随着每一次停机,对替代方案的需求将变得越来越迫切。
另一方面,如果我们能够以某种方式使一切都可以通过不可用的外部服务来工作,那么我们一定会这样做。 每次故障的事后分析对我们有帮助。 总会有一个结论。 这表示您喜欢或不喜欢-我们始终会提供一种解决方法。
在最后一部分中,我将讨论另一种中断类型,以及我们对这些中断所得出的结论,如何修改开发过程以及引入了哪些自动化。 敬请期待!