1 分布式和集群
分布式:把一個系統拆分為多個子系統,每個子系統負責各自的那部分功能,獨立部署,各司其職。
集群:多個實例共同工作,最簡單的集群是把一個應用復制多份部署。
分布式一定是集群,但是集群不一定是分布式(集群就是多個實例一起工作,分布式將一個系統拆分之后那就是多個實例;集群並不一定是分布式,因為復制型的集群不是拆分而是復制)。
2 一致性hash算法
首先了解下為什么使用hash?hash算法較多的應用在數據存儲和查找領域,最經典的是hash表,查詢效率非常之高,數據查詢時間復雜度可以接近0(1)。
2.1 hash算法在分布式集群中的應用場景
hash算法在很多分布式集群產品中都有應用,比如分布式集群架構Redis、Hadoop、elasticSearch,MySQL分庫分表、Nginx負載均衡等。
主要的應用場景:
1)請求的負載均衡(比如Nginx的ip_hash策略)
Nginx的hash策略可以在客戶端IP不變的情況下,將其發出的請求始終路由到同一個目標服務器上,實現回話粘滯,避免處理session共享問題。
如果沒有ip_hash策略,如何實現回話粘滯?可以維護一個映射表,存儲客戶端IP或者sessionid與具體目標服務器的映射關系。但是在客戶端很多的情況下,映射表非常大,浪費內存空間,另一方面,客戶端上下線,目標服務器上下線都會導致重新維護映射表,映射表維護成本很大。
使用哈希算法,可以對IP地址或者sessionid進行計算哈希值,哈希值與服務器數量進行取模運算,得到的值就是當前請求應該被路由到的服務器編號,如此,同一個客戶端IP發送過來的請求就可以路由到同一個目標服務器,實現回話粘滯。
2)分布式存儲
以分布式內存數據庫Redis為例,集群中有redis1,redis2,redis3三台Redis服務器,進行數據存儲時存儲到那台服務器呢,針對key進行hash處理hash(key1)% 3 = index,使用余數index鎖定存儲的具體服務器節點。
2.2 普通hash算法存在的問題
以ip_hash為例,假定下載用戶IP固定沒有發生改變,其中一個tomcat出現了問題,宕機了,服務器數量減少,之前的求模都需要重新計算。
在后台服務器很多台,客戶端也有很多,那么影響非常大,縮容和擴容都會存在這樣的問題,大量用戶的請求會被路由到其他的目標服務器處理,用戶在原來服務器中的回話都會丟失。
2.3 一致性hash算法
首先有一條直線,直線開頭和結分別定位1和2的32次方減一,相當於一個地址,對於這樣一條線,彎過來構成一個圓環形成閉環,這樣的一個圓環稱為hash環,把服務器IP或者主機名求hash值然后對應到hash環上,針對客戶端用戶,可以根據它的IP進行hash求值,對應到某個位置,然后如何確定一個客戶端路由到哪個服務器處理呢?按照順時針方向找最近的服務器節點。
假如將服務器3下線,原來路由到3的客戶端重新路由到4,對於其他客戶端沒有影響,只是小部分受影響(請求的遷移達到了最小,對於分布式集群來說非常合適,避免了大量請求遷移)。
每一台服務器負責一段,一致性哈希算法對於節點的增減都只需重定位環空間中的一小部分數據,具有較好的容錯性和可擴展性。
但是容易因為節點分布不均造成數據傾斜問題,例如有兩台服務器,可能一個節點負責小的一段,大量的客戶端請求落在了另一個節點,這就是數據(請求)傾斜問題。
解決數據傾斜問題,引入虛擬節點機制,即對每一個服務節點計算多個哈希,每個計算結果位置都放置一個此服務節點,稱為虛擬節點。
具體做法可以在服務器IP或主機名的后面增加編號來實現,比如,可以為每台服務器計算三個虛擬節點,於是可以分別計算節點1的IP#1、節點1的IP#2,節點1的IP#3等等,於是形成六個虛擬節點,當客戶端被路由到虛擬節點的時候其實是被路由到該虛擬節點所對應的真實節點。
如下面代碼為簡單的模擬含虛擬節點的一致性hash算法實現。
//1.初始化:把服務器節點IP的哈希值對應到哈希環上 //定義服務器ip String[] tomcatServers = new String[] {"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"}; SortedMap<Integer,String> hashServerMap = new TreeMap<>(); //定義針對每個真實服務器虛擬出來幾個節點 int virtaulCount = 3; for (String tomcatServer : tomcatServers) { //求出每一個ip的hash值,對應到hash環上存儲hash值和ip的對應關系 int serverHash = Math.abs(tomcatServer.hashCode()); //存儲hash值與ip的對應關系 hashServerMap.put(serverHash, tomcatServer); //處理虛擬節點 for (int i = 0; i < virtaulCount; i++) { int virtualHash = Math.abs((tomcatServer + "#" + i).hashCode()); hashServerMap.put(virtualHash, "---由虛擬節點" + i + "映射過來的請求" + tomcatServer); } } //2.針對客戶端ip求出hash值 //定義客戶端ip String[] clients = new String[] {"10.78.12.3","113.25.63.1","126.12.3.8"}; for (String client : clients) { int clientHash = Math.abs(client.hashCode()); //3.針對客戶端,找到能夠處理當前客戶端請求的服務器(哈希環上順時針最近) SortedMap<Integer, String> integerStringSortedMap = hashServerMap.tailMap(clientHash); if (integerStringSortedMap.isEmpty()) { //取哈希環上的順時針第一台服務器 Integer firstkey = hashServerMap.firstKey(); System.out.println("------>>>客戶端:" + client + "被路由到服務器" + hashServerMap.get(firstkey)); } else { Integer firstkey = integerStringSortedMap.firstKey(); System.out.println("------>>>客戶端:" + client + "被路由到服務器" + hashServerMap.get(firstkey)); } }
3 時鍾不同導致的問題
時鍾此處指服務器時間,如果集群中各個服務器時鍾不一致勢必導致一系列問題,舉例電商網站業務中,新增一條訂單,那么勢必會在訂單表中增加一條記錄,該條記錄中應該會有“下單時間”這樣的字段,往往我們會在程序中獲取當前系統時間插入到數據庫或者直接從數據庫服務器獲取時間,訂單子系統是集群化部署,或者數據庫也是分庫分表的集群化部署,如果系統時鍾不一致,那下單時間就不會准確,數據就會混亂。
3.1 集群時鍾同步配置
1)分布式集群中各個服務器 節點都可以連接互聯網
#使用ntpdate網絡時間同步命令
ntpdate -u ntp.api.bz #從一個時間服務器同步時間
Linux可以使用定時任務crond,每隔10分鍾執行一次ntpdate命令。
2)分布式集群中某一個服務器節點可以訪問互聯網或者所有節點都不能夠訪問互聯網
選取集群中的一個服務器節點A作為時間服務器(整個集群時間從這台服務器同步,如果這台服務器能訪問互聯網,可以讓這台服務器和網絡時間保持同步,如果不能就手動設置一個時間)。
首先設置A的時間,把A(172.17.0.17)設置為時間服務器(修改/etc/ntp.conf文件)
1.如果有 restrict default ignore,注釋掉它
2.添加如下幾行內容 restrict 172.17.0.0 mask 255.255.255.0 nomodify notrap #放開局域網同步功能,172.17.0.0為局域網網段
server 127.127.1.0 # local clock
fudge 127.127.1.0 stratum 10
3.重啟生效並配置ntpd服務開機自啟動
service ntpd restart
chkconfig ntpd on
集群中其他節點就可以從A服務器同步時間了ntpdate 172.17.0.17
4 分布式id解決方案
分布式id:分布式集群環境下的全局唯一id。
1)UUID
UUID是指Universally Unique Identifier(通用唯一識別碼)
public static void main(String[] args) { //b4b7a44a-f855-4e59-8e3c-84321e9c9b37s System.out.println(UUID.randomUUID().toString()); }
2)獨立數據庫的自增id
創建數據庫實例global_id_generator,創建了數據庫表,表結構如下:
DROP TABLE IF EXISTS `DISTRIBUTE_ID`;
CREATE TABLE `DISTRIBUTE_ID` (
`id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`createtime` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
當分布式集群環境哪個應用需要獲取一個全局唯一的分布式id的時候,就可以使用代碼連接這個數據庫實例,執行如下sql語句。
insert into DISTRIBUTE_ID(createtime) values(NOW());
select LAST_INSERT_ID();
注意:createtime字段無實際意義,這種方式性能和可靠性都不夠好,需要代碼連接到數據庫才能獲取到id,性能無法保障,如果數據庫掛掉了,就無法獲取分布式id了。
3)SnowFlake雪花算法
是Twitter推出的一個用於生分布式id的策略,生成的id是long型,8個字節,有64bit。
其中符號位固定為0,二進制表示最高位是符號位,時間戳:41個二進制數用來記錄時間戳,表示某一個毫秒,機器id:代表當前算法運行機器的id,序列化:12位用來記錄某個機器一個毫秒內產生的不同序列號,代表同一個機器同一毫秒可以產生的id序號。
4)借助Redis的incr命令獲取全局唯一id
將key中儲存的數字值增一,如果key不存在,那么key的值會被初始化為0,然后再執行incr操作。
5 session共享問題
session共享及session保存或者叫做session一致性。會有什么問題呢?http協議是無狀態的協議,客戶端和服務器在某次回話中產生的數據不會被保留下來,所以第二次請求服務器無法認識到曾經來過。
5.1 解決session一致性的方案
1)nginx的ip_hash策略
同一個客戶端ip的請求會被路由到同一個目標服務器,優點:配置簡單,不入侵應用,不需要額外修改代碼。缺點:服務器重啟session丟失,存在單點負載高的風險,單點故障問題。
2)session復制
多個tomcat之間通過修改配置文件,達到session之間的復制。優點:不入侵應用,便於服務器水平擴展,能適應各種負載均衡策略,服務器重啟或者宕機不會造成session丟失。缺點:性能低,內存消耗,不能存儲太多數據,數據越多越影響性能,延遲性。
3)session共享,session集中存儲
交由Redis,優點:適應各種負載均衡策略,服務器重啟或者宕機不會造成session丟失,擴展能力強,適合大集群數量使用,缺點:對應用入侵,引入和Redis交互代碼。