此處我說的 HTTP 服務主要指如訪問京東網站時我們看到的熱門搜索、用戶登錄、實時價格、實時庫存、服務支持、廣告語等這種非 Web 頁面,而是在 Web 頁面中異步加載的相關數據。這些服務有個特點即訪問量巨大、邏輯比較單一;但是如實時庫存邏輯其實是非常復雜的。在京東這些服務每天有幾億十幾億的訪問量,比如實時庫存服務曾經在沒有任何 IP 限流、DDos 防御的情況被刷到600多萬/分鐘的訪問量,而且能輕松應對。支撐如此大的訪問量就需要考慮設計良好的架構,并很容易實現水平擴展。
此處介紹下我曾使用過 Nginx+JavaEE 的架構。
http://wiki.jikexueyuan.com/project/nginx-lua/images/1.png" alt="" />
早期架構可能就是 Nginx 直接 upstream 請求到后端 Tomcat,擴容時基本是增加新的 Tomcat 實例,然后通過 Nginx 負載均衡 upstream 過去。此時數據庫還不是瓶頸。當訪問量到一定級別,數據庫的壓力就上來了,此處單純的靠單個數據庫可能扛不住了,此時可以通過數據庫的讀寫分離或加緩存來實現。
http://wiki.jikexueyuan.com/project/nginx-lua/images/2.png" alt="" />
此時就通過使用如數據庫讀寫分離或者 Redis 這種緩存來支撐更大的訪問量。使用緩存這種架構會遇到的問題諸如緩存與數據庫數據不同步造成數據不一致(一般設置過期時間),或者如 Redis 掛了,此時會直接命中數據庫導致數據庫壓力過大;可以考慮 Redis 的主從或者一致性Hash 算法做分片的 Redis 集群;使用緩存這種架-構要求應用對數據的一致性要求不是很高;比如像下訂單這種要落地的數據不適合用 Redis 存儲,但是訂單的讀取可以使用緩存。
http://wiki.jikexueyuan.com/project/nginx-lua/images/3.png" alt="" />
首先 Nginx 通過 Lua 讀取本機 Redis 緩存,如果不命中才回源到后端 Tomcat 集群;后端Tomcat 集群再讀取 Mysql 數據庫。Redis 都是安裝到和 Nginx 同一臺服務器,Nginx 直接讀本機可以減少網絡延時。Redis 通過主從方式同步數據,Redis 主從一般采用樹的方式實現:
http://wiki.jikexueyuan.com/project/nginx-lua/images/4.png" alt="" />
在葉子節(jié)點可以做 AOF 持久化,保證在主 Redis 掛時能進行恢復;此處假設對 Redis 很依賴的話,可以考慮多主 Redis 架構,而不是單主,來防止單主掛了時數據的不一致和擊穿到后端Tomcat 集群。這種架構的缺點就是要求 Redis 實例數據量較小,如果單機內存不足以存儲這么多數據,當然也可以通過如尾號為 1 的在 A 服務器,尾號為 2 的在 B 服務器這種方式實現;缺點也很明顯,運維復雜、擴展性差。
http://wiki.jikexueyuan.com/project/nginx-lua/images/5.png" alt="" />
和之前架構不同的點是此時我們使用一致性 Hash 算法實現 Redis 集群而不是讀本機 Redis,保證其中一臺掛了,只有很少的數據會丟失,防止擊穿到數據庫。Redis 集群分片可以使用Twemproxy;如果 Tomcat 實例很多的話,此時就要考慮 Redis 和 Mysql 鏈接數問題,因為大部分 Redis/Mysql 客戶端都是通過連接池實現,此時的鏈接數會成為瓶頸。一般方法是通過中間件來減少鏈接數。
http://wiki.jikexueyuan.com/project/nginx-lua/images/6.png" alt="" />
Twemproxy 與 Redis 之間通過單鏈接交互,并 Twemproxy 實現分片邏輯;這樣我們可以水平擴展更多的 Twemproxy 來增加鏈接數。
此時的問題就是 Twemproxy 實例眾多,應用維護配置困難;此時就需要在之上做負載均衡,比如通過 LVS/HAProxy 實現 VIP(虛擬 IP ),可以做到切換對應用透明、故障自動轉移;還可以通過實現內網 DNS 來做其負載均衡。
http://wiki.jikexueyuan.com/project/nginx-lua/images/7.png" alt="" />
本文沒有涉及 Nginx 之上是如何架構的,對于 Nginx、Redis、Mysql 等的負載均衡、資源的CDN 化不是本文關注的點,有興趣可以參考
Nginx/LVS/HAProxy 負載均衡軟件的優(yōu)缺點詳解
接下來我們來搭建一下第四種架構。
http://wiki.jikexueyuan.com/project/nginx-lua/images/8.png" alt="" />
以獲取如京東商品頁廣告詞為例,如下圖
http://wiki.jikexueyuan.com/project/nginx-lua/images/9.png" alt="" />
假設京東有10億商品,那么廣告詞極限情況是10億;所以在設計時就要考慮:
而對于本例,廣告詞更新量不會很大,每分鐘可能在幾萬左右;而且是 K-V 的,其實適合使用關系存儲;因為廣告詞是商家維護,因此后臺查詢需要知道這些商品是哪個商家的;而對于前臺是不關心商家的,是 KV 存儲,所以前臺顯示的可以放進如 Redis 中。 即存在兩種設計:
基本數據結構:商品 ID、廣告詞、所屬商家、開始時間、結束時間、是否有效。
為了簡單,我們不進行后臺的設計實現,只做前端的設計實現,此時數據結構我們簡化為[商品ID、廣告詞]。另外有朋友可能看到了,可以直接把 Tomcat 部分干掉,通過 Lua 直接讀取Mysql 進行回源實現。為了完整性此處我們還是做回源到 Tomcat 的設計,因為如果邏輯比較復雜的話或一些限制(比如使用 Java 特有協(xié)議的 RPC)還是通過 Java 去實現更方便一些。
項目部署目錄結構。
Java 代碼
/usr/chapter6
redis_6660.conf
redis_6661.conf
nginx_chapter6.conf
nutcracker.yml
nutcracker.init
webapp
WEB-INF
lib
classes
web.xml
此處根據實際情況來決定 Redis 大小,此處我們已兩個 Redis 實例(6660、6661),在Twemproxy 上通過一致性 Hash 做分片邏輯。
之前已經介紹過 Redis 和 Twemproxy 的安裝了。
Redis配置redis_6660.conf和redis_6661.conf
Java 代碼
\#分別為6660 6661
port 6660
\#進程ID 分別改為redis_6660.pid redis_6661.pid
pidfile "/var/run/redis_6660.pid"
\#設置內存大小,根據實際情況設置,此處測試僅設置20mb
maxmemory 20mb
\#內存不足時,按照過期時間進行LRU刪除
maxmemory-policy volatile-lru
\#Redis的過期算法不是精確的而是通過采樣來算的,默認采樣為3個,此處我們改成10
maxmemory-samples 10
\#不進行RDB持久化
save “”
\#不進行AOF持久化
appendonly no
將如上配置放到 redis_6660.conf 和 redis_6661.conf 配置文件最后即可,后邊的配置會覆蓋前邊的。
Twemproxy配置nutcracker.yml
Java 代碼
server1:
listen: 127.0.0.1:1111
hash: fnv1a_64
distribution: ketama
redis: true
timeout: 1000
servers:
- 127.0.0.1:6660:1 server1
- 127.0.0.1:6661:1 server2
復制 nutcracker.init 到 /usr/chapter6 下,并修改配置文件為 /usr/chapter6/nutcracker.yml。
Java 代碼
nohup /usr/servers/redis-2.8.19/src/redis-server /usr/chapter6/redis_6660.conf &
nohup /usr/servers/redis-2.8.19/src/redis-server /usr/chapter6/redis_6661.conf &
/usr/chapter6/nutcracker.init start
ps -aux | grep -e redis -e nutcracker
Atlas 類似于 Twemproxy,是 Qihoo 360 基于 Mysql Proxy 開發(fā)的一個 Mysql 中間件,據稱每天承載讀寫請求數達幾十億,可以實現分表、讀寫分離、數據庫連接池等功能,缺點是沒有實現跨庫分表(分庫)功能,需要在客戶端使用分庫邏輯。另一個選擇是使用如阿里的 TDDL,它是在客戶端完成之前說的功能。到底選擇是在客戶端還是在中間件根據實際情況選擇。
此處我們不做 Mysql 的主從復制(讀寫分離),只做分庫分表實現。
為了測試我們此處分兩個表。
Java 代碼
CREATE DATABASE chapter6 DEFAULT CHARACTER SET utf8;
use chapter6;
CREATE TABLE chapter6.ad_0(
sku_id BIGINT,
content VARCHAR(4000)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE chapter6.ad_1
sku_id BIGINT,
content VARCHAR(4000)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Java 代碼
cd /usr/servers/
wget https://github.com/Qihoo360/Atlas/archive/2.2.1.tar.gz -O Atlas-2.2.1.tar.gz
tar -xvf Atlas-2.2.1.tar.gz
cd Atlas-2.2.1/
\#Atlas依賴mysql_config,如果沒有可以通過如下方式安裝
apt-get install libmysqlclient-dev
\#安裝Lua依賴
wget http://www.lua.org/ftp/lua-5.1.5.tar.gz
tar -xvf lua-5.1.5.tar.gz
cd lua-5.1.5/
make linux && make install
\#安裝glib依賴
apt-get install libglib2.0-dev
\#安裝libevent依賴
apt-get install libevent
\#安裝flex依賴
apt-get install flex
\#安裝jemalloc依賴
apt-get install libjemalloc-dev
\#安裝OpenSSL依賴
apt-get install openssl
apt-get install libssl-dev
apt-get install libssl0.9.8
./configure --with-mysql=/usr/bin/mysql_config
./bootstrap.sh
make && make install
Java 代碼
vim /usr/local/mysql-proxy/conf/chapter6.cnf
Java代碼 收藏代碼
[mysql-proxy]
\#Atlas代理的主庫,多個之間逗號分隔
proxy-backend-addresses = 127.0.0.1:3306
\#Atlas代理的從庫,多個之間逗號分隔,格式ip:port@weight,權重默認1
\#proxy-read-only-backend-addresses = 127.0.0.1:3306,127.0.0.1:3306
\#用戶名/密碼,密碼使用/usr/servers/Atlas-2.2.1/script/encrypt 123456加密
pwds = root:/iZxz+0GRoA=
\#后端進程運行
daemon = true
\#開啟monitor進程,當worker進程掛了自動重啟
keepalive = true
\#工作線程數,對Atlas的性能有很大影響,可根據情況適當設置
event-threads = 64
\#日志級別
log-level = message
\#日志存放的路徑
log-path = /usr/chapter6/
\#實例名稱,用于同一臺機器上多個Atlas實例間的區(qū)分
instance = test
\#監(jiān)聽的ip和port
proxy-address = 0.0.0.0:1112
\#監(jiān)聽的管理接口的ip和port
admin-address = 0.0.0.0:1113
\#管理接口的用戶名
admin-username = admin
\#管理接口的密碼
admin-password = 123456
\#分表邏輯
tables = chapter6.ad.sku_id.2
\#默認字符集
charset = utf8
因為本例沒有做讀寫分離,所以讀庫 proxy-read-only-backend-addresses 沒有配置。分表邏輯即:數據庫名.表名.分表鍵.表的個數,分表的表名格式是 table_N,N 從 0 開始。
Java 代碼
/usr/local/mysql-proxy/bin/mysql-proxyd chapter6 start
/usr/local/mysql-proxy/bin/mysql-proxyd chapter6 restart
/usr/local/mysql-proxy/bin/mysql-proxyd chapter6 stop
如上命令會自動到 /usr/local/mysql-proxy/conf 目錄下查找 chapter6.cnf 配置文件。
通過如下命令進入管理接口
Java 代碼
mysql -h127.0.0.1 -P1113 -uadmin -p123456
通過執(zhí)行 SELECT * FROM help 查看幫助。還可以通過一些 SQL 進行服務器的動態(tài)添加/移除。
通過如下命令進入客戶端接口
Java 代碼
mysql -h127.0.0.1 -P1112 -uroot -p123456
Java 代碼
use chapter6;
insert into ad values(1 '測試1);
insert into ad values(2, '測試2');
insert into ad values(3 '測試3);
select * from ad where sku_id=1;
select * from ad where sku_id=2;
#通過如下sql可以看到實際的分表結果
select * from ad_0;
select * from ad_1;
此時無法執(zhí)行 select from ad,需要使用如 “select from ad where sku_id=1” 這種 SQL 進行查詢;即需要帶上 sku_id 且必須是相等比較;如果是范圍或模糊是不可以的;如果想全部查詢,只能挨著遍歷所有表進行查詢。即在客戶端做查詢-聚合。
此處實際的分表邏輯是按照商家進行分表,而不是按照商品編號,因為我們后臺查詢時是按照商家維度的,此處是為了測試才使用商品編號的。
到此基本的 Atlas 就介紹完了,更多內容請參考如下資料: Mysql 主從復制 http://369369.blog.51cto.com/319630/790921/ Mysql中間件介紹 http://www.guokr.com/blog/475765/ Atlas使用 http://www.0550go.com/database/mysql/mysql-atlas.html Atlas文檔 https://github.com/Qihoo360/Atlas/blob/master/README_ZH.md
Java 安裝
Java 代碼
cd /usr/servers/
\#首先到如下網站下載JDK
\#http://www.oracle.com/technetwork/cn/java/javase/downloads/jdk7-downloads-1880260.html
\#本文下載的是 jdk-7u75-linux-x64.tar.gz。
tar -xvf jdk-7u75-linux-x64.tar.gz
vim ~/.bashrc
在文件最后添加如下環(huán)境變量
export JAVA_HOME=/usr/servers/jdk1.7.0_75/
export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH
export CLASSPATH=$CLASSPATH:.:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
\#使環(huán)境變量生效
source ~/.bashrc
Java 代碼
cd /usr/servers/
wget http://ftp.cuhk.edu.hk/pub/packages/apache.org/tomcat/tomcat-7/v7.0.59/bin/apache-tomcat-7.0.59.tar.gz
tar -xvf apache-tomcat-7.0.59.tar.gz
cd apache-tomcat-7.0.59/
\#啟動
/usr/servers/apache-tomcat-7.0.59/bin/startup.sh
\#停止
/usr/servers/apache-tomcat-7.0.59/bin/shutdown.sh
\#刪除tomcat默認的webapp
rm -r apache-tomcat-7.0.59/webapps/*
\#通過Catalina目錄發(fā)布web應用
cd apache-tomcat-7.0.59/conf/Catalina/localhost/
vim ROOT.xml
ROOT.xml
Java 代碼
<!-- 訪問路徑是根,web應用所屬目錄為/usr/chapter6/webapp -->
<Context path="" docBase="/usr/chapter6/webapp"></Context>
Java 代碼
\#創(chuàng)建一個靜態(tài)文件隨便添加點內容
vim /usr/chapter6/webapp/index.html
\#啟動
/usr/servers/apache-tomcat-7.0.59/bin/startup.sh
訪問如 http://192.168.1.2:8080/index.html 能處理內容說明配置成功。
Java 代碼
\#變更目錄結構
cd /usr/servers/
mv apache-tomcat-7.0.59 tomcat-server1
\#此處我們創(chuàng)建兩個tomcat實例
cp –r tomcat-server1 tomcat-server2
vim tomcat-server2/conf/server.xml
Java 代碼
\#如下端口進行變更
8080--->8090
8005--->8006
啟動兩個 Tomcat
Java 代碼
/usr/servers/tomcat-server1/bin/startup.sh
/usr/servers/tomcat-server2/bin/startup.sh
分別訪問,如果能正常訪問說明配置正常。
http://192.168.1.2:8080/index.htmlhttp://192.168.1.2:8090/index.html如上步驟使我們在一個服務器上能啟動兩個 tomcat 實例,這樣的好處是我們可以做本機的 Tomcat 負載均衡,假設一個 tomcat 重啟時另一個是可以工作的,從而不至于不給用戶返回響應。
我們使用 Maven 搭建 Web 項目,Maven 知識請自行學習。
本文將最小化依賴,即僅依賴我們需要的 servlet、mysql、druid、jedis。
Java 代碼
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.27</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.5</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.5.2</version>
</dependency>
</dependencies>
com.github.zhangkaitao.chapter6.servlet.AdServlet
Java 代碼
public class AdServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String idStr = req.getParameter("id");
Long id = Long.valueOf(idStr);
//1、讀取Mysql獲取數據
String content = null;
try {
content = queryDB(id);
} catch (Exception e) {
e.printStackTrace();
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
if(content != null) {
//2.1、如果獲取到,異步寫Redis
asyncSetToRedis(idStr, content);
//2.2、如果獲取到,把響應內容返回
resp.setCharacterEncoding("UTF-8");
resp.getWriter().write(content);
} else {
//2.3、如果獲取不到,返回404狀態(tài)碼
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
private DruidDataSource datasource = null;
private JedisPool jedisPool = null;
{
datasource = new DruidDataSource();
datasource.setUrl("jdbc:mysql://127.0.0.1:1112/chapter6?useUnicode=true&characterEncoding=utf-8&autoReconnect=true");
datasource.setUsername("root");
datasource.setPassword("123456");
datasource.setMaxActive(100);
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxTotal(100);
jedisPool = new JedisPool(poolConfig, "127.0.0.1", 1111);
}
private String queryDB(Long id) throws Exception {
Connection conn = null;
try {
conn = datasource.getConnection();
String sql = "select content from ad where sku_id = ?";
PreparedStatement psst = conn.prepareStatement(sql);
psst.setLong(1, id);
ResultSet rs = psst.executeQuery();
String content = null;
if(rs.next()) {
content = rs.getString("content");
}
rs.close();
psst.close();
return content;
} catch (Exception e) {
throw e;
} finally {
if(conn != null) {
conn.close();
}
}
}
private ExecutorService executorService = Executors.newFixedThreadPool(10);
private void asyncSetToRedis(final String id, final String content) {
executorService.submit(new Runnable() {
@Override
public void run() {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.setex(id, 5 * 60, content);//5分鐘
} catch (Exception e) {
e.printStackTrace();
jedisPool.returnBrokenResource(jedis);
} finally {
jedisPool.returnResource(jedis);
}
}
});
}
}
整個邏輯比較簡單,此處更新緩存一般使用異步方式去更新,這樣不會阻塞主線程;另外此處可以考慮走 Servlet 異步化來提示吞吐量。
web.xml 配置
Java 代碼
<servlet>
<servlet-name>adServlet</servlet-name>
<servlet-class>com.github.zhangkaitao.chapter6.servlet.AdServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>adServlet</servlet-name>
<url-pattern>/ad</url-pattern>
</servlet-mapping>
打 WAR 包
Java 代碼
cd D:\workspace\chapter6
mvn clean package
此處使用maven命令打包,比如本例將得到chapter6.war,然后將其上傳到服務器的/usr/chapter6/webapp,然后通過unzip chapter6.war解壓。
啟動 Tomcat 實例,分別訪問如下地址將看到廣告內容:
Java 代碼
http://192.168.1.2:8080/ad?id=1
http://192.168.1.2:8090/ad?id=1
vim /usr/chapter6/nginx_chapter6.conf
Java 代碼
upstream backend {
server 127.0.0.1:8080 max_fails=5 fail_timeout=10s weight=1 backup=false;
server 127.0.0.1:8090 max_fails=5 fail_timeout=10s weight=1 backup=false;
check interval=3000 rise=1 fall=2 timeout=5000 type=tcp default_down=false;
keepalive 100;
}
server {
listen 80;
server_name _;
location ~ /backend/(.*) {
keepalive_timeout 30s;
keepalive_requests 100;
rewrite /backend(/.*) $1 break;
#之后該服務將只有內部使用,ngx.location.capture
proxy_pass_request_headers off;
#more_clear_input_headers Accept-Encoding;
proxy_next_upstream error timeout;
proxy_pass http://backend;
}
}
upstream 配置:http://nginx.org/cn/docs/http/ngx_http_upstream_module.html。
server:指定上游到的服務器, weight:權重,權重可以認為負載均衡的比例; fail_timeout+max_fails:在指定時間內失敗多少次認為服務器不可用,通過proxy_next_upstream來判斷是否失敗。
check:ngx_http_upstream_check_module模塊,上游服務器的健康檢查,interval:發(fā)送心跳包的時間間隔,rise:連續(xù)成功rise次數則認為服務器up,fall:連續(xù)失敗fall次則認為服務器down,timeout:上游服務器請求超時時間,type:心跳檢測類型(比如此處使用 tcp )更多配置請參考 [https://github.com/yaoweibin/ nginx_upstream_check_module](https://github.com/yaoweibin/ nginx_upstream_check_module) 和 http://tengine.taobao.org/document_cn/http_upstream_check_cn.html。
keepalive:用來支持 upstream server http keepalive 特性(需要上游服務器支持,比如 tomcat )。默認的負載均衡算法是 round-robin,還可以根據 ip、url 等做 hash 來做負載均衡。更多資料請參考官方文檔。
tomcat keepalive 配置: http://tomcat.apache.org/tomcat-7.0-doc/config/http.html。 maxKeepAliveRequests:默認100; keepAliveTimeout:默認等于 connectionTimeout,默認 60 秒;
location proxy 配置:http://nginx.org/cn/docs/http/ngx_http_proxy_module.html。 rewrite:將當前請求的 url 重寫,如我們請求時是 /backend/ad,則重寫后是 /ad。 proxy_pass:將整個請求轉發(fā)到上游服務器。 proxy_next_upstream:什么情況認為當前 upstream server 失敗,需要 next upstream,默認是連接失敗/超時,負載均衡參數。 proxy_pass_request_headers:之前已經介紹過了,兩個原因:1、假設上游服務器不需要請求頭則沒必要傳輸請求頭;2、ngx.location.capture時防止 gzip 亂碼(也可以使用more_clear_input_headers 配置)。 keepalive:keepalive_timeout:keepalive 超時設置,keepalive_requests:長連接數量。此處的 keepalive(別人訪問該 location 時的長連接)和 upstream keepalive(nginx 與上游服務器的長連接)是不一樣的;此處注意,如果您的服務是面向客戶的,而且是單個動態(tài)內容就沒必要使用長連接了。
vim /usr/servers/nginx/conf/nginx.conf
Java 代碼
include /usr/chapter6/nginx_chapter6.conf;
\#為了方便測試,注釋掉example.conf
\#include /usr/example/example.conf;
重啟 nginx
/usr/servers/nginx/sbin/nginx -s reload
訪問如 192.168.1.2/backend/ad?id=1 即看到結果??梢?kill 掉一個 tomcat,可以看到服務還是正常的。
vim /usr/chapter6/nginx_chapter6.conf
Java 代碼
location ~ /backend/(.*) {
internal;
keepalive_timeout 30s;
keepalive_requests 1000;
#支持keep-alive
proxy_http_version 1.1;
proxy_set_header Connection "";
rewrite /backend(/.*) $1 break;
proxy_pass_request_headers off;
#more_clear_input_headers Accept-Encoding;
proxy_next_upstream error timeout;
proxy_pass http://backend;
}
加上 internal,表示只有內部使用該服務。
核心代碼
/usr/chapter6/ad.lua
Java 代碼
local redis = require("resty.redis")
local cjson = require("cjson")
local cjson_encode = cjson.encode
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
local ngx_exit = ngx.exit
local ngx_print = ngx.print
local ngx_re_match = ngx.re.match
local ngx_var = ngx.var
local function close_redis(red)
if not red then
return
end
--釋放連接(連接池實現)
local pool_max_idle_time = 10000 --毫秒
local pool_size = 100 --連接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx_log(ngx_ERR, "set redis keepalive error : ", err)
end
end
local function read_redis(id)
local red = redis:new()
red:set_timeout(1000)
local ip = "127.0.0.1"
local port = 1111
local ok, err = red:connect(ip, port)
if not ok then
ngx_log(ngx_ERR, "connect to redis error : ", err)
return close_redis(red)
end
local resp, err = red:get(id)
if not resp then
ngx_log(ngx_ERR, "get redis content error : ", err)
return close_redis(red)
end
--得到的數據為空處理
if resp == ngx.null then
resp = nil
end
close_redis(red)
return resp
end
local function read_http(id)
local resp = ngx.location.capture("/backend/ad", {
method = ngx.HTTP_GET,
args = {id = id}
})
if not resp then
ngx_log(ngx_ERR, "request error :", err)
return
end
if resp.status ~= 200 then
ngx_log(ngx_ERR, "request error, status :", resp.status)
return
end
return resp.body
end
--獲取id
local id = ngx_var.id
--從redis獲取
local content = read_redis(id)
--如果redis沒有,回源到tomcat
if not content then
ngx_log(ngx_ERR, "redis not found content, back to http, id : ", id)
content = read_http(id)
end
--如果還沒有返回404
if not content then
ngx_log(ngx_ERR, "http not found content, id : ", id)
return ngx_exit(404)
end
--輸出內容
ngx.print("show_ad(")
ngx_print(cjson_encode({content = content}))
ngx.print(")")
將可能經常用的變量做成局部變量,如 local ngx_print = ngx.print;使用 jsonp 方式輸出,此處我們可以將請求 url 限定為 /ad/id 方式,這樣的好處是1、可以盡可能早的識別無效請求;2、可以走 nginx 緩存 /CDN 緩存,緩存的 key 就是 URL,而不帶任何參數,防止那些通過加隨機數穿透緩存;3、jsonp 使用固定的回調函數 show_ad(),或者限定幾個固定的回調來減少緩存的版本。
vim /usr/chapter6/nginx_chapter6.conf
Java 代碼
location ~ ^/ad/(\d+)$ {
default_type 'text/html';
charset utf-8;
lua_code_cache on;
set $id $1;
content_by_lua_file /usr/chapter6/ad.lua;
}
重啟 nginx
Java 代碼
/usr/servers/nginx/sbin/nginx -s reload
訪問如 http://192.168.1.2/ad/1 即可得到結果。而且注意觀察日志,第一次訪問時不命中Redis,回源到 Tomcat;第二次請求時就會命中 Redis 了。
第一次訪問時將看到 /usr/servers/nginx/logs/error.log 輸出類似如下的內容,而第二次請求相同的 url 不再有如下內容:
Java 代碼
redis not found content, back to http, id : 2
到此整個架構就介紹完了,此處可以直接不使用 Tomcat,而是 Lua 直連 Mysql 做回源處理;另外本文只是介紹了大體架構,還有更多業(yè)務及運維上的細節(jié)需要在實際應用中根據自己的場景自己摸索。后續(xù)如使用 LVS/HAProxy 做負載均衡、使用 CDN 等可以查找資料學習。