在本系列的第一部分中,讨论了如何将Apache+mod_php替换为Nginx+PHP-FPM。本文将探讨当将LAMP堆栈迁移到可扩展架构时需要考虑的高级主题。
坦白说,这一步比前两步要困难得多。虽然没有代码更改,但配置和思维方式可能会显得陌生且难以设置。一旦克服了这个障碍,保证永远不会想回去。
假设有一个名为Box 1的服务器,上面运行着应用程序代码和MySQL数据库。如果想添加另一个服务器,Box 2,最初的方法可能是在它上面放置应用程序代码和MySQL数据库。这对于读取操作是完全可以的,但对于写入操作遇到了问题。如果Box 1的应用程序代码在其数据库上运行插入/更新/删除查询,如何在Box 2上运行该查询?一个糟糕的方法是更改数据库接口类,使其在多个服务器上运行这些查询。需要一种在多个服务器之间复制查询的方法。
MySQL有一种设置主从复制的方法,即在主服务器上运行的所有查询也会在从服务器上运行。这意味着Box 2的查询可以在其数据库上运行,但插入/更新/删除查询需要指向Box 1。再次,这是一个需要代码更改的糟糕方法。它还让依赖于单点故障。
下一个迭代是尝试主主复制。在一个服务器上运行的查询将自动传播到其他服务器。这最终是理想的解决方案。摆脱了单点故障,并且可以与应用程序代码相同的服务器上的数据库进行通信。不幸的是,MySQL没有很好的方法来实现这种复制。
进入Percona XtraDB Cluster。
Percona是一家专注于改进MySQL的公司。他们定期分支公共MySQL代码,并对其进行增强,使其更快、更适合扩展。他们对MySQL缺乏有效的主主复制的解决方案是XtraDB Cluster。当设置一个运行该服务的集群时,本地节点上运行的查询将被发送到其他节点,并且会创建一个块。在其他节点成功执行该查询后,块将被解除。
Percona XtraDB Cluster很酷。真的非常酷。因为它是MySQL的一个分支,所以连接到它并在上面执行查询就像在应用程序代码上使用标准MySQL一样。所有Percona MySQL分支都旨在更快,这是锦上添花。它还无缝处理节点启动和关闭时自动递增ID。本地节点生成的ID永远不会发生冲突。
XtraBackup工具允许几乎实时创建备份。
使用XtraDB Cluster时需要考虑一些事项。MyISAM表不会在集群中复制。它们需要转换为InnoDB。如果复合主键顺序不正确(自动递增字段不是第一个),这可能是一个问题。听说他们计划在未来添加MyISAM支持,但真的不应该在生产环境中使用MyISAM。它在插入/更新/删除时使用表级锁定,而InnoDB是行级。
下一个考虑是需要的节点数量。为了使查询成功执行,必须有超过51%的节点能够执行它。一个或两个节点无法实现法定人数,所以三个是最小数量。建议使用大于三的奇数作为集群中的节点数量。好消息是,节点离线然后重新上线几乎可以无缝处理。如果启动集群的节点关闭,其配置文件需要稍微更改,以便它不会创建新集群,而是加入现有集群。
最后一个考虑可能不适用于所有设置。在使用prepared statements和mysqlnd作为连接器时遇到了一些问题。当XtraDB Cluster作为单个实例运行时,具有prepared变量的concat内的查询会静默失败。
将其提交为bug,但没有人能够复现它。
PHP的会话是救星。之前使用的是基于Cookie的方法,直到意识到PHP已经支持保存用户特定数据。PHP会话通过检查一个cookie(默认为PHPSESSID)来工作。如果该cookie未设置,它将生成一个唯一的cookie。如果已设置,它将在所有后续请求中发送到服务器。这样用户就可以被唯一识别。会话特定数据可以放入$_SESSION变量中。
PHP用于保存会话数据的默认机制是通过文件系统。这对于LAMP模型效果很好,但对于用户可能击中不同盒子的架构来说并不适用。为了扩展会话,需要一种在盒子之间共享会话数据的方法。
解决这个问题的第一次尝试是使用Memcached。设置了一个Memcached集群,安装了一个Memcached会话插件,并更改了php.ini以使用它作为会话处理器。这种方法的最大缺点是优雅地处理故障。Memcached不持久化到磁盘。如果服务器关闭,所有会话数据基本上就消失了。在处理的应用程序中,这不是一个可接受的解决方案。
下一次尝试涉及使用Couchbase。它是CouchDB的一个实现,其接口与Memcached完全相同。它会将会话数据写入磁盘,所以关闭不是问题。Couchbase的缺点是其性能成本。它会迅速消耗内存和CPU,原因不明。这也不可接受。
第三次尝试涉及使用Memcached和MongoDB。较新版本的PHP允许定义自己的会话处理器。可以给它在打开、读取、写入、关闭和销毁时调用的方法。编写了自己的类,从Memcached读取,但如果缓存未命中,它会尝试MongoDB。这个类占用空间很小,但需要完美地编写会话类。它在缓存未命中时也非常慢。
第四次尝试是使用Memcached和Redis。Redis允许持久化到磁盘,并且比MongoDB快得多。这里最大的缺点是使用两种基于RAM的技术而不是一种。
选择的解决方案是自定义Redis会话保存处理器。Redis集群仅通过分片支持复制。需要相同的数据集在每个机器上存在,所以分片不是一个选项。不得不编写自定义会话保存处理器来在集群的所有节点上复制。这个类大约有300行代码,需要很多工作,但它做得很好。
有一个社区维护的Redis会话保存处理器扩展可以使用。如果分片是一个选项,会使用它。它比自定义处理器快,因为它是用C而不是PHP编写的。
细节决定成败。这是生活中的许多事情的情况,包括扩展。如果按照本指南进行,现在已经有了一个非常坚实的PHP应用程序基础,而无需任何代码更改。(耶!)还有一些障碍需要克服,但幸运的是最难的部分已经过去了。
LAMP模型很好,因为只有三个配置文件需要维护。在提到的架构中,必须维护Nginx、PHP-FPM、PHP、Percona XtraDB Cluster和PHP会话保存处理器。然后乘以堆栈中的机器数量。有很多需要维护的东西,希望有一些魔法豆可以建议来解决所有问题。
除了为它构建一个特定的机制之外,没有找到一个好的机制来管理所有这些文件。在所有盒子上更新应用程序代码也是一个挑战。CI工具如Jenkins和Capistrano在这方面可以提供帮助。没有太多设置这方面的经验,所以祝好运。
在LAMP模型中编写的代码假设应用程序代码和文件系统总是绑定在一起。在扩展模型中,情况并非如此。这意味着存储在文件系统中的缓存或上传的文件可能不会在集群中的每台机器上存在。如果有一个数据库记录包含上传文件的路径,它可能在一台机器上工作,但在另一台机器上不工作。仍然不确定解决这个问题的最佳方法。能提供的最佳解决方案是使用一个守护进程,监听特定目录中的变化,并与集群中的其他服务器同步。使用Lsync和csync2是推荐的方法。