nginx另类复杂的架构
本章以京东商品详情页为例,京东商品详情页虽然仅是单个页面,但是其数据聚合源是非常多的,除了一些实时性要求比较高的如价格、库存、服务支持等通过AJAX异步加载加载之外,其他的数据都是在后端做数据聚合然后拼装网页模板的。
http://item.jd.com/1217499.html
如图所示,商品页主要包括商品基本信息(基本信息、图片列表、颜色/尺码关系、扩展属性、规格参数、包装清单、售后保障等)、商品介绍、其他信息(分类、品牌、店铺【第三方卖家】、店内分类【第三方卖家】、同类相关品牌)。更多细节此处就不阐述了。
整个京东有数亿商品,如果每次动态获取如上内容进行模板拼装,数据来源之多足以造成性能无法满足要求;最初的 解决方案是生成静态页,但是静态页的最大的问题:1、无法迅速响应页面需求变更;2、很难做多版本线上对比测试。如上两个因素足以制约商品页的多样化发 展,因此静态化技术不是很好的方案。
通过分析,数据主要分为四种:商品页基本信息、商品介绍(异步加载)、其他信息(分类、品牌、店铺等)、其他 需要实时展示的数据(价格、库存等)。而其他信息如分类、品牌、店铺是非常少的,完全可以放到一个占用内存很小的Redis中存储;而商品基本信息我们可 以借鉴静态化技术将数据做聚合存储,这样的好处是数据是原子的,而模板是随时可变的,吸收了静态页聚合的优点,弥补了静态页的多版本缺点;另外一个非常严 重的问题就是严重依赖这些相关系统,如果它们挂了或响应慢则商品页就挂了或响应慢;商品介绍我们也通过AJAX技术惰性加载(因为是第二屏,只有当用户滚 动鼠标到该屏时才显示);而实时展示数据通过AJAX技术做异步加载;因此我们可以做如下设计:
1、接收商品变更消息,做商品基本信息的聚合,即从多个数据源获取商品相关信息如图片列表、颜色尺码、规格参 数、扩展属性等等,聚合为一个大的JSON数据做成数据闭环,以key-value存储;因为是闭环,即使依赖的系统挂了我们商品页还是能继续服务的,对 商品页不会造成任何影响;
2、接收商品介绍变更消息,存储商品介绍信息;
3、介绍其他信息变更消息,存储其他信息。
整个架构如下图所示:
技术选型
MQ可以使用如Apache ActiveMQ;
Worker/动态服务可以通过如Java技术实现;
RPC可以选择如alibaba Dubbo;
KV持久化存储可以选择SSDB(如果使用SSD盘则可以选择SSDB+RocksDB引擎)或者ARDB(LMDB引擎版);
缓存使用Redis;
SSDB/Redis分片使用如Twemproxy,这样不管使用Java还是Nginx+Lua,它们都不关心分片逻辑;
前端模板拼装使用Nginx+Lua;
数据集群数据存储的机器可以采用RAID技术或者主从模式防止单点故障;
因为数据变更不频繁,可以考虑SSD替代机械硬盘。
核心流程
1、首先我们监听商品数据变更消息;
2、接收到消息后,数据聚合Worker通过RPC调用相关系统获取所有要展示的数据,此处获取数据的来源可能非常多而且响应速度完全受制于这些系统,可能耗时几百毫秒甚至上秒的时间;
3、将数据聚合为JSON串存储到相关数据集群;
4、前端Nginx通过Lua获取相关集群的数据进行展示;商品页需要获取基本信息+其他信息进行模板拼装, 即拼装模板仅需要两次调用(另外因为其他信息数据量少且对一致性要求不高,因此我们完全可以缓存到Nginx本地全局内存,这样可以减少远程调用提高性 能);当页面滚动到商品介绍页面时异步调用商品介绍服务获取数据;
5、如果从聚合的SSDB集群/Redis中获取不到相关数据;则回源到动态服务通过RPC调用相关系统获取 所有要展示的数据返回(此处可以做限流处理,因为如果大量请求过来的话可能导致服务雪崩,需要采取保护措施),此处的逻辑和数据聚合Worker完全一 样;然后发送MQ通知数据变更,这样下次访问时就可以从聚合的SSDB集群/Redis中获取数据了。
基本流程如上所述,主要分为Worker、动态服务、数据存储和前端展示;因为系统非常复杂,只介绍动态服务和前端展示、数据存储架构;Worker部分不做实现。
项目搭建
项目部署目录结构。
/usr/chapter7
ssdb_basic_7770.conf
ssdb_basic_7771.conf
ssdb_basic_7772.conf
ssdb_basic_7773.conf
ssdb_desc_8880.conf
ssdb_desc_8881.conf
ssdb_desc_8882.conf
ssdb_desc_8883.conf
redis_other_6660.conf
redis_other_6661.conf
nginx_chapter7.conf
nutcracker.yml
nutcracker.init
item.html
header.html
footer.html
item.lua
desc.lua
lualib
item.lua
item
common.lua
webapp
WEB-INF
lib
classes
web.xml
数据存储实现
整体架构为主从模式,写数据到主集群,读数据从从集群读取数据,这样当一个集群不足以支撑流量时可以使用更多的集群来支撑更多的访问量;集群分片使用Twemproxy实现。
商品基本信息SSDB集群配置
vim /usr/chapter7/ssdb_basic_7770.conf
work_dir = /usr/data/ssdb_7770 pidfile = /usr/data/ssdb_7770.pid server: ip: 0.0.0.0 port: 7770 allow: 127.0.0.1 allow: 192.168 replication: binlog: yes sync_speed: -1 slaveof: logger: level: error output: /usr/data/ssdb_7770.log rotate: size: 1000000000 leveldb: cache_size: 500 block_size: 32 write_buffer_size: 64 compaction_speed: 1000 compression: yes
vim /usr/chapter7/ssdb_basic_7771.conf
work_dir = /usr/data/ssdb_7771 pidfile = /usr/data/ssdb_7771.pid server: ip: 0.0.0.0 port: 7771 allow: 127.0.0.1 allow: 192.168 replication: binlog: yes sync_speed: -1 slaveof: logger: level: error output: /usr/data/ssdb_7771.log rotate: size: 1000000000 leveldb: cache_size: 500 block_size: 32 write_buffer_size: 64 compaction_speed: 1000 compression: yes
vim /usr/chapter7/ssdb_basic_7772.conf
work_dir = /usr/data/ssdb_7772 pidfile = /usr/data/ssdb_7772.pid server: ip: 0.0.0.0 port: 7772 allow: 127.0.0.1 allow: 192.168 replication: binlog: yes sync_speed: -1 slaveof: type: sync ip: 127.0.0.1 port: 7770 logger: level: error output: /usr/data/ssdb_7772.log rotate: size: 1000000000 leveldb: cache_size: 500 block_size: 32 write_buffer_size: 64 compaction_speed: 1000 compression: yes
vim /usr/chapter7/ssdb_basic_7773.conf
work_dir = /usr/data/ssdb_7773 pidfile = /usr/data/ssdb_7773.pid server: ip: 0.0.0.0 port: 7773 allow: 127.0.0.1 allow: 192.168 replication: binlog: yes sync_speed: -1 slaveof: type: sync ip: 127.0.0.1 port: 7771 logger: level: error output: /usr/data/ssdb_7773.log rotate: size: 1000000000 leveldb: cache_size: 500 block_size: 32 write_buffer_size: 64 compaction_speed: 1000 compression: yes
配置文件使用Tab而不是空格做缩排,(复制到配置文件后请把空格替换为Tab)。主从关系:7770(主)-->7772(从),7771(主)--->7773(从);配置文件如何配置请参考https://github.com/ideawu/ssdb-docs/blob/master/src/zh_cn/config.md。
创建工作目录
mkdir -p /usr/data/ssdb_7770 mkdir -p /usr/data/ssdb_7771 mkdir -p /usr/data/ssdb_7772 mkdir -p /usr/data/ssdb_7773
启动
nohup /usr/servers/ssdb-1.8.0/ssdb-server /usr/chapter7/ssdb_basic_7770.conf & nohup /usr/servers/ssdb-1.8.0/ssdb-server /usr/chapter7/ssdb_basic_7771.conf & nohup /usr/servers/ssdb-1.8.0/ssdb-server /usr/chapter7/ssdb_basic_7772.conf & nohup /usr/servers/ssdb-1.8.0/ssdb-server /usr/chapter7/ssdb_basic_7773.conf &
通过ps -aux | grep ssdb命令看是否启动了,tail -f nohup.out查看错误信息。
商品介绍SSDB集群配置
vim /usr/chapter7/ssdb_desc_8880.conf
work_dir = /usr/data/ssdb_8880 pidfile = /usr/data/ssdb8880.pid server: ip: 0.0.0.0 port: 8880 allow: 127.0.0.1 allow: 192.168 replication: binlog: yes sync_speed: -1 slaveof: logger: level: error output: /usr/data/ssdb_8880.log rotate: size: 1000000000 leveldb: cache_size: 500 block_size: 32 write_buffer_size: 64 compaction_speed: 1000 compression: yes
vim /usr/chapter7/ssdb_desc_8881.conf
work_dir = /usr/data/ssdb_8881 pidfile = /usr/data/ssdb8881.pid server: ip: 0.0.0.0 port: 8881 allow: 127.0.0.1 allow: 192.168 logger: level: error output: /usr/data/ssdb_8881.log rotate: size: 1000000000 leveldb: cache_size: 500 block_size: 32 write_buffer_size: 64 compaction_speed: 1000 compression: yes
vim /usr/chapter7/ssdb_desc_8882.conf
work_dir = /usr/data/ssdb_8882 pidfile = /usr/data/ssdb_8882.pid server: ip: 0.0.0.0 port: 8882 allow: 127.0.0.1 allow: 192.168 replication: binlog: yes sync_speed: -1 slaveof: replication: binlog: yes sync_speed: -1 slaveof: type: sync ip: 127.0.0.1 port: 8880 logger: level: error output: /usr/data/ssdb_8882.log rotate: size: 1000000000 leveldb: cache_size: 500 block_size: 32 write_buffer_size: 64 compaction_speed: 1000 compression: yes
vim /usr/chapter7/ssdb_desc_8883.conf
work_dir = /usr/data/ssdb_8883 pidfile = /usr/data/ssdb_8883.pid server: ip: 0.0.0.0 port: 8883 allow: 127.0.0.1 allow: 192.168 replication: binlog: yes sync_speed: -1 slaveof: type: sync ip: 127.0.0.1 port: 8881 logger: level: error output: /usr/data/ssdb_8883.log rotate: size: 1000000000 leveldb: cache_size: 500 block_size: 32 write_buffer_size: 64 compaction_speed: 1000 compression: yes
配置文件使用Tab而不是空格做缩排(复制到配置文件后请把空格替换为Tab)。主从关系:7770(主)-->7772(从),7771(主)--->7773(从);配置文件如何配置请参考https://github.com/ideawu/ssdb-docs/blob/master/src/zh_cn/config.md。
创建工作目录
mkdir -p /usr/data/ssdb_888{0,1,2,3}
启动
nohup /usr/servers/ssdb-1.8.0/ssdb-server /usr/chapter7/ssdb_desc_8880.conf & nohup /usr/servers/ssdb-1.8.0/ssdb-server /usr/chapter7/ssdb_desc_8881.conf & nohup /usr/servers/ssdb-1.8.0/ssdb-server /usr/chapter7/ssdb_desc_8882.conf & nohup /usr/servers/ssdb-1.8.0/ssdb-server /usr/chapter7/ssdb_desc_8883.conf &
通过ps -aux | grep ssdb命令看是否启动了,tail -f nohup.out查看错误信息。
其他信息Redis配置
vim /usr/chapter7/redis_6660.conf
port 6660 pidfile "/var/run/redis_6660.pid" #设置内存大小,根据实际情况设置,此处测试仅设置20mb maxmemory 20mb #内存不足时,所有KEY按照LRU算法删除 maxmemory-policy allkeys-lru #Redis的过期算法不是精确的而是通过采样来算的,默认采样为3个,此处我们改成10 maxmemory-samples 10 #不进行RDB持久化 save “” #不进行AOF持久化 appendonly no
vim /usr/chapter7/redis_6661.conf
port 6661 pidfile "/var/run/redis_6661.pid" #设置内存大小,根据实际情况设置,此处测试仅设置20mb maxmemory 20mb #内存不足时,所有KEY按照LRU算法进行删除 maxmemory-policy allkeys-lru #Redis的过期算法不是精确的而是通过采样来算的,默认采样为3个,此处我们改成10 maxmemory-samples 10 #不进行RDB持久化 save “” #不进行AOF持久化 appendonly no #主从 slaveof 127.0.0.1 6660