引言
在扩展大量大型的分布式系统期间,我有机会观察(并实践)了一些最差实践。这些最差实践中的大部分在开始时都没有危害,但如果疏忽大意,它们就会对系统的发展和可伸缩性构成危害。很多文章都聚焦于最佳实践,以确保拥有一个易于维护和可伸缩的系统,但在本文中,我主要强调的则是一些应该规避的最差实践。
技术
没有任何一种技术或架构能实现所有的需求。了解何时该反思现有的方法、如何拓宽视野以超越局部范围、或如何进行依赖的有效控制,这些都是可伸缩性的关键特性。让我们进一步分别研究一下。
金锤子
金锤子起源于一条古老的谚语:如果你只有一把锤子,那么任何东西在你眼里都是一枚钉子。很多">开发人员都局限在仅使用一种技术的观念中——其代价是不得不使用选定的技术来构建和维护基础设施,即便已经存在另一种技术更适用于特定问题域的功能和抽象。强行把一种技术用在它所不擅长的方面,有时会适得其反。
举例来说,持久化键—值对问题的常见解决方案是使用数据库。之所以常常这样选择是因为组织或开发者有坚实的数据库实践,针对许多问题自然而然就会沿用同样的解决途径。当数据库的特性(关系完整性、锁、连接和方案)成为瓶颈或阻碍了其伸缩扩展时,问题也就出现了。这是因为应用基于数据库的解决方案要发展,其成本通常要比使用其它可用技术更为昂贵。随着键—值存储访问率的增加,数据库并发模式的性能就开始降低,而数据库具备的高级特性却被闲置。许多传统关系数据库的替代方案都是针对这些缺点的,比如CouchDB、SimpleDB或BigTable。
另一个常见的“锤子”就是总利用线程来进行并发编程。尽管线程确实是针对并发的,但它们也带来了成本,这些成本包括代码复杂性的增加、以及由于目前线程的的锁定和访问模型造成的组件编排(composability )方面的固有不足。由于如今最流行的编程语言都使用线程处理并发,因此数千行代码都含有竞态条件、潜在的死锁和不一致的数据访问管理。有些正在成长的社区提出了另一些并发方案,这些方案不存在线程的可伸缩性问题,也就是由Erlang或Stackless Python提倡的并发模型。即便不在实际生产中选择那些语言,研究一下它们的概念(比如消息传递或异步I/O)仍然是一种不错的实践。
资源滥用
小范围的问题开发者们一般都能处理得得心应手:使用分析工具、了解算法的空间和时间复杂度、或者了解哪种场合应该用哪种列表实现。但并非每个人都善于认识到大型系统的约束条件,比如识别共享资源的性能要求、了解服务的各种客户、或发掘数据库的访问模式。
应用程序实现伸缩性的普遍方法是不断横向部署冗余的、无状态的、彼此不共享内容的服务,以此作为最理想的体系架构。但以我的经验看来,这种扩展往往会忽视新增服务对共享资源的影响。
比如说,如果一个特定的服务使用数据库作为持久存储,它通常通过一个线程池来管理数据库连接。使用池是不错的方法,有助于避免进行过多的数据库连接处理。然而数据库仍然是共享资源,除了单个池配置,还必须对所有池从总体上进行管理。下面两个实践就会导致失败:
持续增加服务数,但并不减小池的最大数。
增大单个池的大小,而不减小服务数量。
以上两种情况中,除了按性能要求配置应用之外,连接的总数也必须加以管理。此外,还要持续监控数据库的容量,以保持连接均衡。
处理共享资源的可用性至关重要,准确的说,这是因为它们一旦失效,由于其“共享”的本质,失效会对系统造成全面的影响,而非孤立存在。
大泥球
依赖是很多系统讨厌却又必不可少的东西,不积极地处理好依赖及其版本会损害灵活性和可伸缩性。
代码的依赖管理有多种不同的模式:
同时编译整个代码集
基于已知版本选取构件和服务
发布的模型和服务所有变更都向后兼容
让我们看看这些情形。首先,在大泥球模式下,整个系统作为一个单元编译和部署。这种模式拥有明显的优势,也就是将依赖管理交给编译器处理,并能提前捕获一些问题,但它会因每次都部署整个系统(包括测试、交付和大范围变化引起的风险)而引发可伸缩性的问题。在这种模式下,会更难隔离系统的变化。