以下是这次 Think in LAMP 2012.6 演讲的内容整理。
公司概况
安居客集团(Anjuke Inc.)旗下有四个主要网站:安居客(anjuke.com)、爱房网(aifang.com)、好租(haozu.com)和金铺(jinpu.com),同时提供 iOS、Android、Windows Phone 等多平台的移动应用。
截至 2012 年,安居客每月有 3000 万独立访客,每天 2000 万页面浏览量,近 200 位工程师,约 150 台服务器分布在 3 个 IDC。
从小到大
安居客的技术栈一直是经典的 LAMP(Linux、Apache、MySQL、PHP)。随着业务的增长,架构经历了持续的演进:
最初只有 1 个 IDC、2 台服务器。随着流量从 20 万增长到 200 万(10 倍),我们依次引入了数据库主从复制(DB Replication)、从 LIKE% 模糊查询迁移到 Solr 搜索引擎、网站 V2 重构、Squid 缓存代理、Memcached 内存缓存、分布式图片存储和 RabbitMQ 消息队列,最终实施了数据库分片(DB Sharding)。到这个阶段,已经扩展到 2 个 IDC、17 台服务器。
进入第二个 10 倍增长阶段(200 万到 2000 万),基础设施进一步扩展到 3 个 IDC、150 台服务器。在这个过程中,架构的几个核心子系统逐步成熟:数据库、搜索、缓存、图片系统和消息队列。
搜索架构
那么我们用上 Solr 不就好了吗?
事情没那么简单。安居客的搜索面临着持续的复杂性增长:
- 一开始只有一个索引——房源
- 索引逐渐多起来——经纪人、地图、论坛、搜索提示……
- 索引的文档数量增大,需要拆分成多个
- 一个网站变为多个——安居客、爱房、好租、金铺、移动
- 业务调整频繁,索引经常要重建
- 越来越多 Solr 的实例需要管理
Solr Cloud
为了解决这些问题,我们构建了一套自研的 Solr Cloud 管理平台,可以通过 Web 界面自助创建和管理 Solr 实例。
核心设计是一个 Service 多个 Instance。每个搜索服务(如”安居客二手房搜索-上海”)可以有多个实例运行在不同的物理机器上,支持在线查看和修改 schema,支持实例的动态调配。
最终的规模:
- 77 个不同的索引
- 98 个运行中的 Solr 实例
- 部署在 12 台物理机器上
- 每天约 8000 万次搜索
空闲的 Solr 实例可以随时按需分配,动态调整。
图片系统
图片是房产网站的核心资产。安居客的图片系统规模相当可观:
- 每天新上传 120 万张照片
- 每天图片请求约 1.4 亿次
- 约 4 亿张图片,44TB 存储
- 部署在独立的 IDC,40 多台 PC 服务器
- 通过 CDN 分发
图片系统既简单又复杂。我们的改造经历了几个阶段:从最初的 htdocs/images 目录,到独立的 img1~n 服务器,再到与应用解耦,最终形成了目前的架构。整个系统提供标准化的服务接口,支持定制化的尺寸和水印处理。
底层存储使用 MogileFS,架构分为上传路径(Upload → Image Processor → MogileFS)和展示路径(Display → Cache Farm → Load Balancers)。
MogileFS 的使用策略
- 只保存原图:每张图片保存 2 份副本,使用廉价的 SATA 硬盘和 XFS 文件系统
- 以内容 hash 值作为文件名:由于业务特性,超过 60% 的新上传图片是重复的,通过 hash 去重可以大量节省存储空间
- 显示时才处理图片:切图和打水印在请求时按需进行,处理好的图片在前端缓存
- 支持多个网站:同一张在安居客上传的图片,可以在好租网以好租的尺寸和水印展示
- 疑似虚假图片识别:对新上传的图片打上 Tag,可用于识别重复的和带水印的图片
从小到大的过程中
在架构演进过程中,有几个心得:
- 选择简单实用的方案
- 逐步改进,不用一步到位
- 让重复的劳动自动化
关注开发和测试
网站的架构不仅是这些生产环境的组件。开发和测试同样是架构的重要组成部分。
源代码管理
源代码的版本管理遵循以下原则:
- master 保持稳定
- 项目在各自的 feature branch 开发
- 功能测试通过后 merge 回主干
- 紧急的缺陷在 release branch 上修改
我们从 Subversion 迁移到了 Git。Git 支持离线工作,分支管理更加灵活。内部搭建了 GitCorp——一个类似 GitHub 的代码托管平台,提供代码浏览、用户管理和活动统计等功能。
同时推行了 Code Review(非强制),通过 Review Board 工具进行代码评审。
测试环境
受 12-Factor App 的启发,我们的理念是:开发、测试、生产环境越接近越好。
- 开发环境:
.$username.dev.anjuke.com - 测试环境:
.$fp#.qa.anjuke.com - 生产环境:
.anjuke.com
每个开发者有自己独立的开发环境,测试环境按功能点(feature point)编号分配。
TiP: Test in Production
我们的架构支持在生产环境中进行测试——灰度发布:
- 多版本布署:机器上同时有多个版本的代码,由配置指明应该运行哪个版本
- 指定运行版本:每个人可以指定不同的版本(通过 HTTP 头
m=app10-019, v=20120605_03) - 指定运行机器:还可以指定请求落在哪台服务器上
- Beta 和 GA:在办公室内部总是优先访问 Beta 版本
这套机制让我们可以小范围内测新功能,逐步扩大灰度范围,出现问题时快速回滚。
架构应该包括开发和测试。
利用数据和工具
衡量架构的效果,作为改进的依据。
我们建设了多个层次的数据和工具体系:
基础监控:Cacti
通过 Cacti 监控所有服务器的网络带宽、CPU 使用率和负载等基础指标。
数据库监控:DBMO
DBMO(Database Manage And Operation System)是我们自研的数据库管理和运维系统,提供:
- 数据库使用概况——读写量趋势、各业务数据库占比
- 每台数据库服务器的详细监控——Load Average、CPU、IO 等
- 表行数 Top 10、表大小 Top 10
- 日志告警(如 DB Partition 95%)
缓存管理:Memcached / Varnish
为 Memcached 和 Varnish 搭建了统一的管理界面,可以查看各实例的命中率、内存使用、Slab 分布等详细统计。
定制图表:pyfisheyes
pyfisheyes 是一套自定义的监控图表系统,用于展示负载均衡器的平均执行时间、各服务器的 CPU Iowait、System、User 等细粒度指标。
DW/BI 业务分析中心
MAX(安居客集团数据分析中心)是一个数据驱动的业务智能平台,覆盖集团、安居客、爱房、好租、金铺、移动各事业部,提供流量 Dashboard、月流量趋势、访客浏览器分析、日流量 Scorecard、市场渠道分析等定制报表。
PHP 性能分析:Performance Analysis Dashboard
这是一个我觉得很有意思的数据。我们自研了 Performance Analysis Dashboard,可以回答:
- 多数用户的响应时间是多少?
- 90% 的用户响应时间是多少?
- 具体到每个 Controller 的处理时间分布如何?
可以下钻到每个请求的详细情况,查看不同应用、不同机器上的平均响应时间、Controller 处理时间、数据库查询时间,甚至逐条 SQL 的执行时间和占比。系统还支持按 Request List、SQL List、Lowest Top 10、Largest Difference Top 10 等维度查询。
数据和工具很有帮助。架构的设计要考虑数据的采集。
持续改进
乔老板都说了:Stay hungry, Stay foolish。
APS: 异步 P2P 服务
APS 是一套基于 ØMQ 的 P2P 消息队列和异步远过程调用系统。
为什么需要异步?
考虑一个典型的页面请求,需要依次获取问题详细信息(10ms)、获取回答列表(15ms)、获取相关问题列表(30ms)。串行执行的总耗时是:
10 + 15 + 30 = 55 (ms)
如果将没有依赖关系的调用并行执行——获取问题详细信息和获取回答列表仍然串行(有依赖),同时并行获取相关问题列表:
max(10 + 15, 30) = 30 (ms)
耗时从 55ms 降低到 30ms,几乎减半。
APS 的设计
APS 的核心功能包括:
- P2P 的消息发送:点对点通信,不需要中心 Broker
- PHP 的异步方法调用:在 PHP 中实现非阻塞的远程过程调用
- 虚拟的消息总线:跨多个 IDC 的服务互联
架构上,PHP 通过 zmq 连接 Dispatcher,Dispatcher 通过 zmq 连接后端 Service,同时有 Config Agent 负责配置管理和 Service Registry 负责服务注册发现。整套系统支持跨 IDC 部署。
PHP 客户端只需要 4 个函数:
publish($subject, $content);
request($service, $method, $params);
start_request($service, $method, $params, $callback);
wait_for_replies($timeout);
通过 start_request() 发起异步请求,继续执行其他逻辑,最后通过 wait_for_replies() 等待所有异步请求返回。
总结
我对网站架构的体会是:
- 选择简单实用的方案
- 同时关注开发和测试
- 利用数据和工具
- 持续改进