此處我說的HTTP服務主要指如訪問京東網站時我們看到的熱門搜索、用戶登錄、實時價格、實時庫存、服務支持、廣告語等這種非Web頁面,而是在Web頁面中異步加載的相關數據。這些服務有個特點即訪問量巨大、邏輯比較單一;但是如實時庫存邏輯其實是非常復雜的。在京東這些服務每天有幾億十幾億的訪問量,比如實時庫存服務曾經在沒有任何IP限流、DDos防御的情況被刷到600多萬/分鐘的訪問量,而且能輕松應對。支撐如此大的訪問量就需要考慮設計良好的架構,并很容易實現水平擴展。
?
架構
此處介紹下我曾使用過Nginx+JavaEE的架構。
?
1、單DB架構
早期架構可能就是Nginx直接upstream請求到后端Tomcat,擴容時基本是增加新的Tomcat實例,然后通過Nginx負載均衡upstream過去。此時數據庫還不是瓶頸。當訪問量到一定級別,數據庫的壓力就上來了,此處單純的靠單個數據庫可能扛不住了,此時可以通過數據庫的讀寫分離或加緩存來實現。
?
?
2、DB+Cache/數據庫讀寫分離架構
?
此時就通過使用如數據庫讀寫分離或者Redis這種緩存來支撐更大的訪問量。使用緩存這種架構會遇到的問題諸如緩存與數據庫數據不同步造成數據不一致(一般設置過期時間),或者如Redis掛了,此時會直接命中數據庫導致數據庫壓力過大;可以考慮Redis的主從或者一致性Hash 算法做分片的Redis集群;使用緩存這種架-構要求應用對數據的一致性要求不是很高;比如像下訂單這種要落地的數據不適合用Redis存儲,但是訂單的讀取可以使用緩存。
?
3、Nginx+Lua+Local Redis+Mysql集群架構
首先Nginx通過Lua讀取本機Redis緩存,如果不命中才回源到后端Tomcat集群;后端Tomcat集群再讀取Mysql數據庫。Redis都是安裝到和Nginx同一臺服務器,Nginx直接讀本機可以減少網絡延時。Redis通過主從方式同步數據,Redis主從一般采用樹的方式實現:
在葉子節點可以做AOF持久化,保證在主Redis掛時能進行恢復;此處假設對Redis很依賴的話,可以考慮多主Redis架構,而不是單主,來防止單主掛了時數據的不一致和擊穿到后端Tomcat集群。這種架構的缺點就是要求Redis實例數據量較小,如果單機內存不足以存儲這么多數據,當然也可以通過如尾號為1的在A服務器,尾號為2的在B服務器這種方式實現;缺點也很明顯,運維復雜、擴展性差。
?
4、Nginx+Lua+ Redis集群+Mysql集群架構?
和之前架構不同的點是此時我們使用一致性Hash算法實現Redis集群而不是讀本機Redis,保證其中一臺掛了,只有很少的數據會丟失,防止擊穿到數據庫。Redis集群分片可以使用Twemproxy;如果 Tomcat實例很多的話,此時就要考慮Redis和Mysql鏈接數問題,因為大部分Redis/Mysql客戶端都是通過連接池實現,此時的鏈接數會成為瓶頸。一般方法是通過中間件來減少鏈接數。
Twemproxy與Redis之間通過單鏈接交互,并Twemproxy實現分片邏輯;這樣我們可以水平擴展更多的Twemproxy來增加鏈接數。
?
此時的問題就是Twemproxy實例眾多,應用維護配置困難;此時就需要在之上做負載均衡,比如通過LVS/HAProxy實現VIP(虛擬IP),可以做到切換對應用透明、故障自動轉移;還可以通過實現內網DNS來做其負載均衡。
本文沒有涉及Nginx之上是如何架構的,對于Nginx、Redis、Mysql等的負載均衡、資源的CDN化不是本文關注的點,有興趣可以參考
很早的Taobao CDN架構
Nginx/LVS/HAProxy負載均衡軟件的優缺點詳解?
?
實現
接下來我們來搭建一下第四種架構。
以獲取如京東商品頁廣告詞為例,如下圖
假設京東有10億商品,那么廣告詞極限情況是10億;所以在設計時就要考慮:
1、數據量,數據更新是否頻繁且更新量是否很大;
2、是K-V還是關系,是否需要批量獲取,是否需要按照規則查詢。
?
而對于本例,廣告詞更新量不會很大,每分鐘可能在幾萬左右;而且是K-V的,其實適合使用關系存儲;因為廣告詞是商家維護,因此后臺查詢需要知道這些商品是哪個商家的;而對于前臺是不關心商家的,是KV存儲,所以前臺顯示的可以放進如Redis中。 即存在兩種設計:
1、所有數據存儲到Mysql,然后熱點數據加載到Redis;
2、關系存儲到Mysql,而數據存儲到如SSDB這種持久化KV存儲中。
?
基本數據結構:商品ID、廣告詞、所屬商家、開始時間、結束時間、是否有效。
?
后臺邏輯
1、商家登錄后臺;
2、按照商家分頁查詢商家數據,此處要按照商品關鍵詞或商品類目查詢的話,需要走商品系統的搜索子系統,如通過Solr或elasticsearch實現搜索子系統;
3、進行廣告詞的增刪改查;
4、增刪改時可以直接更新Redis緩存或者只刪除Redis緩存(第一次前臺查詢時寫入緩存);
?
前臺邏輯
1、首先Nginx通過Lua查詢Redis緩存;
2、查詢不到的話回源到Tomcat,Tomcat讀取數據庫查詢到數據,然后把最新的數據異步寫入Redis(一般設置過期時間,如5分鐘);此處設計時要考慮假設Tomcat讀取Mysql的極限值是多少,然后設計降級開關,如假設每秒回源達到100,則直接不查詢Mysql而返回空的廣告詞來防止Tomcat應用雪崩。
?
為了簡單,我們不進行后臺的設計實現,只做前端的設計實現,此時數據結構我們簡化為[商品ID、廣告詞]。另外有朋友可能看到了,可以直接把Tomcat部分干掉,通過Lua直接讀取Mysql進行回源實現。為了完整性此處我們還是做回源到Tomcat的設計,因為如果邏輯比較復雜的話或一些限制(比如使用Java特有協議的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+Twemproxy配置
此處根據實際情況來決定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?? ??
Mysql+Atlas配置
Atlas類似于Twemproxy,是Qihoo 360基于Mysql Proxy開發的一個Mysql中間件,據稱每天承載讀寫請求數達幾十億,可以實現分表、分庫(sharding版本)、讀寫分離、數據庫連接池等功能,缺點是沒有實現跨庫分表功能,需要在客戶端使用分庫邏輯,目前Atlas不活躍。另一個選擇是使用如阿里的TDDL,它是在客戶端完成之前說的功能。到底選擇是在客戶端還是在中間件根據實際情況選擇。
?
此處我們不做Mysql的主從復制(讀寫分離),只做分庫分表實現。
?
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;?? ?
Atlas安裝
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?? ?
Atlas配置?
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實例間的區分??instance?=?test??#監聽的ip和port??proxy-address?=?0.0.0.0:1112??#監聽的管理接口的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開始。
?
Atlas啟動/重啟/停止
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配置文件。?
?
Atlas管理
通過如下命令進入管理接口
Java代碼??
mysql?-h127.0.0.1?-P1113??-uadmin?-p123456??? 通過執行SELECT * FROM help查看幫助。還可以通過一些SQL進行服務器的動態添加/移除。
?
Atlas客戶端
通過如下命令進入客戶端接口
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;??? ?
此時無法執行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+Tomcat安裝
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??在文件最后添加如下環境變量??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????#使環境變量生效??source?~/.bashrc?? ??
Tomcat安裝?
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目錄發布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代碼??
#創建一個靜態文件隨便添加點內容??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??#此處我們創建兩個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.html
http://192.168.1.2:8090/index.html
?
如上步驟使我們在一個服務器上能啟動兩個tomcat實例,這樣的好處是我們可以做本機的Tomcat負載均衡,假設一個tomcat重啟時另一個是可以工作的,從而不至于不給用戶返回響應。
?
Java+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狀態碼??????????????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?? ?
nginx配置
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:發送心跳包的時間間隔,rise:連續成功rise次數則認為服務器up,fall:連續失敗fall次則認為服務器down,timeout:上游服務器請求超時時間,type:心跳檢測類型(比如此處使用tcp)更多配置請參考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:將整個請求轉發到上游服務器。
??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與上游服務器的長連接)是不一樣的;此處注意,如果您的服務是面向客戶的,而且是單個動態內容就沒必要使用長連接了。
?
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,表示只有內部使用該服務。?
?
Nginx+Lua邏輯開發
核心代碼?
/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做回源處理;另外本文只是介紹了大體架構,還有更多業務及運維上的細節需要在實際應用中根據自己的場景自己摸索。后續如使用LVS/HAProxy做負載均衡、使用CDN等可以查找資料學習。
?
- chapter6.zip?(2.8 MB)
- 下載次數: 294
來源:http://jinnianshilongnian.iteye.com/blog/2188113
總結
以上是生活随笔為你收集整理的第六章 Web开发实战1——HTTP服务的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。