Redis Cluster

Redis Cluster

Redis Cluster是Redis官方在Redis 3.0版本正式推出的高可用以及分布式的解決方案。

Redis Cluster由多個(gè)Redis實(shí)例組成的整體,數(shù)據(jù)按照槽(slot)存儲(chǔ)分布在多個(gè)Redis實(shí)例上,通過Gossip協(xié)議來進(jìn)行節(jié)點(diǎn)之間通信。


Redis Cluster

Redis Cluster實(shí)現(xiàn)的功能:

? 將數(shù)據(jù)分片到多個(gè)實(shí)例(按照slot存儲(chǔ));

? 集群節(jié)點(diǎn)宕掉會(huì)自動(dòng)failover;

? 提供相對(duì)平滑擴(kuò)容(縮容)節(jié)點(diǎn)。


Redis Cluster暫未有的:

? 實(shí)時(shí)同步

? 強(qiáng)一致性


Redis Cluster分片實(shí)現(xiàn)

一般分片(Sharding)實(shí)現(xiàn)的方式有l(wèi)ist、range和hash(或者基于上述的組合方式)等方式。而Redis的實(shí)現(xiàn)方式是基于hash的分片方式,具體是虛擬槽分區(qū)。

虛擬槽分區(qū)

槽(slot):使用分散度良好的hash函數(shù)把所有數(shù)據(jù)映射到一個(gè)固定范圍的整數(shù)集合中,這個(gè)整數(shù)集合就是槽。

Redis Cluster槽: Redis Cluster槽的范圍是0 ~ 16383。槽是集群內(nèi)數(shù)據(jù)管理和遷移的基本單位。


分片的具體算法

Redis Cluster使用slot ?= CRC16(key) %16384來計(jì)算鍵key屬于哪個(gè)slot。(Redis先對(duì)key使用CRC16算法計(jì)算出一個(gè)結(jié)果,然后再把結(jié)果對(duì)16384求余,得到結(jié)果即跟Redis Cluster的slot對(duì)應(yīng),也就是對(duì)應(yīng)數(shù)據(jù)存儲(chǔ)的槽數(shù)。)

(注: CRC16算法——循環(huán)冗余校驗(yàn)(Cyclic Redundancy Check/Code),Redis使用的是CRC-16-CCITT標(biāo)準(zhǔn),即G(x)為:x16+?x12+?x5+ 1。)

Redis Cluster中的每個(gè)分片只需要維護(hù)自己的槽以及槽所映射的鍵值數(shù)據(jù)。


Redis Cluster 分片實(shí)現(xiàn)

Hash標(biāo)簽

哈希標(biāo)簽(hash tags),在Redis集群分片中,可以通過哈希標(biāo)簽來實(shí)現(xiàn)指定兩個(gè)及以上的Key在同一個(gè)slot中。只要Key包含“{…}”這種模式,Redis就會(huì)根據(jù)第一次出現(xiàn)的’{’和第一次出現(xiàn)的’}’之間的字符串進(jìn)行哈希計(jì)算以獲取相對(duì)應(yīng)的slot數(shù)。如上Redis源碼實(shí)現(xiàn)。

所以如果要指定某些Key存儲(chǔ)到同一個(gè)slot中,只需要在命令Key的之后指定相同的“{…}”命名模式即可。


Redis 分片源碼


集群節(jié)點(diǎn)和槽

我們現(xiàn)在已經(jīng)知道,Redis Cluster中的keys被分割為16384個(gè)槽(slot),如果一個(gè)槽一個(gè)節(jié)點(diǎn)的話,那Redis Cluster最大的節(jié)點(diǎn)數(shù)量也就是16384個(gè)。官方推薦最大節(jié)點(diǎn)數(shù)量為1000個(gè)左右。

(關(guān)于Redis為什么使用16384個(gè)槽,原作者有回答:why redis-cluster use 16384 slots?



槽與分片節(jié)點(diǎn)

1當(dāng)Redis Cluster中的16384個(gè)槽都有節(jié)點(diǎn)在處理時(shí),集群處于上線狀態(tài)(ok);

如果Redis Cluster中有任何一個(gè)槽沒有得到處理(或者某一分片的最后一個(gè)節(jié)點(diǎn)掛了),那么集群處于下線狀態(tài)(fail)。(info cluster中的:cluster_state狀態(tài))。那整個(gè)集群就不能對(duì)外提供服務(wù)。

Redis-3.0.0.rc1加入cluster-require-full-coverage參數(shù),默認(rèn)關(guān)閉,打開集群容忍部分失敗。

但是如果集群超過半數(shù)以上master掛掉,無論是否有slave集群進(jìn)入fail狀態(tài)。


節(jié)點(diǎn)ID


Redis Cluster每個(gè)節(jié)點(diǎn)在集群中都有唯一的ID,該ID是由40位的16進(jìn)制字符組成,具體是節(jié)點(diǎn)第一次啟動(dòng)由linux的/dev/urandom生成。具體信息會(huì)保存在node.cnf配置文件中(該文件有Redis Cluster自動(dòng)維護(hù),可以通過參數(shù)cluster-config-file來指定路徑和名稱),如果該文件被刪除,節(jié)點(diǎn)ID將會(huì)重新生成。(刪除以后所有的cluster和replication信息都沒有了)或者通過Cluster Reset強(qiáng)制請(qǐng)求硬重置。

節(jié)點(diǎn)ID用于標(biāo)識(shí)集群中的每個(gè)節(jié)點(diǎn),包括指定Replication Master。只要節(jié)點(diǎn)ID不改變,哪怕節(jié)點(diǎn)的IP和端口發(fā)生了改變,Redis Cluster可以自動(dòng)識(shí)別出IP和端口的變化,并將變更的信息通過Gossip協(xié)議廣播給其他節(jié)點(diǎn)。


ClusterNode

ClusterNode 數(shù)據(jù)結(jié)構(gòu)


Master 節(jié)點(diǎn)維護(hù)這一個(gè)16384/8字節(jié)的位序列,Master節(jié)點(diǎn)用bit來標(biāo)識(shí)對(duì)于某個(gè)槽自己是否擁有。(判斷索引是不是為1即可)

slots屬性是一個(gè)二進(jìn)制位數(shù)組(bit arry),這個(gè)數(shù)組的長度為16384/8 = 2048個(gè)字節(jié),共包含16384個(gè)二進(jìn)制。

Redis Cluster對(duì)slots數(shù)組中的16384個(gè)二進(jìn)制位進(jìn)行編號(hào):從0為起始索引,16383為終止索引。

根據(jù)索引i上的二進(jìn)制位的值來判斷節(jié)點(diǎn)是否負(fù)責(zé)處理槽i:

?slots數(shù)組在索引i上的二進(jìn)制位的值為1,即表示該節(jié)點(diǎn)負(fù)責(zé)處理槽i;

?slots數(shù)組在索引i上的二進(jìn)制位的值為0,即表示該節(jié)點(diǎn)不負(fù)責(zé)處理槽i;

示例1:(如下節(jié)點(diǎn)負(fù)責(zé)處理slot0-slot7)

示例1

即在Redis Cluster中Master節(jié)點(diǎn)使用bit(0)和bit(1)來標(biāo)識(shí)對(duì)某個(gè)槽是否擁有,而Master只要判斷序列第二位的值是不是1即可,時(shí)間復(fù)雜度為O(1)。

numslots屬性記錄節(jié)點(diǎn)負(fù)責(zé)處理的槽的數(shù)量,也就是slots數(shù)組中值為1的二進(jìn)制位的數(shù)量。上圖中節(jié)點(diǎn)處理的槽數(shù)量為8個(gè)。


ClusterState

集群中所有槽的分配信息都保存在ClusterState數(shù)據(jù)結(jié)構(gòu)的slots數(shù)組中,程序要檢查槽i是否已經(jīng)被分配或者找出處理槽i的節(jié)點(diǎn),只需要訪問clusterState.slots[i]的值即可,時(shí)間復(fù)雜度為O(1)。

slots數(shù)組包含16384個(gè)項(xiàng),每個(gè)數(shù)組項(xiàng)都是一個(gè)指向clusterNode結(jié)構(gòu)的指針:

?如果slots[i]指針指向null,那么表示槽i尚未指派給任何節(jié)點(diǎn);

?如果slots[i]指針指向一個(gè)clusterNode結(jié)構(gòu),那么表示槽i已經(jīng)指派給了clusterNode結(jié)構(gòu)所代表的節(jié)點(diǎn)。


數(shù)據(jù)結(jié)構(gòu) ClusterState


示例2:

1. ?slots[0]至slots[4999]的指針都指向端口為6381的節(jié)點(diǎn),即槽0到4999都由節(jié)點(diǎn)6381負(fù)責(zé)處理;

2. ?slots[5000]至slots[9999]的指針都指向端口為6382的節(jié)點(diǎn),即槽5000到9999都由節(jié)點(diǎn)6382負(fù)責(zé)處理;

3.?slots[10000]至slots[16383]的指針都指向端口為6383的節(jié)點(diǎn),即槽10000到16384都由節(jié)點(diǎn)6383負(fù)責(zé)處理。

示例2



數(shù)組 clusterNode.slotsclusterState.slots

? clusterNode.slots數(shù)組記錄了clusterNode結(jié)構(gòu)所代表的節(jié)點(diǎn)的槽指派信息(每個(gè)節(jié)點(diǎn)負(fù)責(zé)哪些槽)。

? clusterState.slots數(shù)組記錄了集群中所有槽的指派信息。

? 如果需要查看某個(gè)節(jié)點(diǎn)的槽指派信息,只需要將相應(yīng)節(jié)點(diǎn)的clusterNode.slots數(shù)組整個(gè)發(fā)送出去即可。

? 但是如果需要查看槽i是否被分配或者分配給了哪個(gè)節(jié)點(diǎn),就需要遍歷clusterState.nodes字典中所有clusterNode結(jié)構(gòu),檢查這些結(jié)構(gòu)的slots數(shù)組,直到遍歷到負(fù)責(zé)處理槽i的節(jié)點(diǎn)為止,這個(gè)過程的時(shí)間復(fù)雜度為O(N),N是clusterState.nodes字典保存的clusterNode結(jié)構(gòu)的數(shù)量。

? 引入clusterState.slots ,將所有槽的指派信息保存在clusterState.slots數(shù)組里面,程序要檢查槽i是否已經(jīng)被指派,或者查看負(fù)責(zé)處理槽i的節(jié)點(diǎn),只需要訪問clusterState.slots[i]的值即可,這個(gè)操作的時(shí)間復(fù)雜度為O(1)。

? 如果只使用clusterState.slots數(shù)組(不引入clusterNode.slots),如果要將節(jié)點(diǎn)A的槽指派信息傳播給其他節(jié)點(diǎn)時(shí),必須先遍歷整個(gè)clusterState.slots數(shù)組,記錄節(jié)點(diǎn)A負(fù)責(zé)處理哪些槽,然后再發(fā)送給其他節(jié)點(diǎn)。比直接發(fā)送clusterNode.slots數(shù)組要低效的多。


Redis Cluster節(jié)點(diǎn)通信

Redis Cluster采用P2P的Gossip協(xié)議,Gossip協(xié)議的原理就是每個(gè)節(jié)點(diǎn)與其他節(jié)點(diǎn)間不斷通信交換信息,一段時(shí)間后節(jié)點(diǎn)信息一致,每個(gè)節(jié)點(diǎn)都知道集群的完整信息。

Redis Cluster通信過程:

(1)集群中的每個(gè)節(jié)點(diǎn)都會(huì)單獨(dú)開辟一個(gè)TCP通道,用于節(jié)點(diǎn)之間彼此通信,通信端口號(hào)在基礎(chǔ)端口上加10000;

(2)每個(gè)節(jié)點(diǎn)在固定周期內(nèi)通過特定規(guī)則選擇幾個(gè)節(jié)點(diǎn)發(fā)送ping消息;

(3)接收到ping消息的節(jié)點(diǎn)用pong消息作為響應(yīng)。

集群中每個(gè)節(jié)點(diǎn)通過一定規(guī)則挑選要通信的節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)可能知道全部節(jié)點(diǎn),也可能僅知道部分節(jié)點(diǎn),

只要這些節(jié)點(diǎn)彼此可以正常通信,最終它們會(huì)達(dá)到一致的狀態(tài)。當(dāng)節(jié)點(diǎn)出故障、新節(jié)點(diǎn)加入、主從角色變化、槽信息變更等事件發(fā)生時(shí),通過不斷的ping/pong消息通信,經(jīng)過一段時(shí)間后所有的節(jié)點(diǎn)都會(huì)知道整個(gè)集群全部節(jié)點(diǎn)的最新狀態(tài),從而達(dá)到集群狀態(tài)同步的目的。


Gossip消息

Gossip協(xié)議的主要職責(zé)就是信息交換,信息交換的載體就是節(jié)點(diǎn)彼此發(fā)送的Gossip消息,常用的Gossip消息可分為:

? meet消息:用于通知新節(jié)點(diǎn)加入。消息發(fā)送者通知接收者加入到當(dāng)前集群,meet消息通信正常完成后,接收節(jié)點(diǎn)會(huì)加入到集群中并進(jìn)行周期性的ping、pong消息交換;

? ping消息:集群內(nèi)交換最頻繁的消息,集群內(nèi)每個(gè)節(jié)點(diǎn)每秒向多個(gè)其他節(jié)點(diǎn)發(fā)送ping消息,用于檢測(cè)節(jié)點(diǎn)是否在線和交換彼此狀態(tài)信息。ping消息發(fā)送封裝了自身節(jié)點(diǎn)和部分其他節(jié)點(diǎn)的狀態(tài)數(shù)據(jù)。

? pong消息:當(dāng)接收到ping、meet消息時(shí),作為響應(yīng)消息回復(fù)給發(fā)送方確認(rèn)消息正常通信。pong消息內(nèi)部封裝了自身狀態(tài)數(shù)據(jù)。節(jié)點(diǎn)也可以向集群內(nèi)廣播自身的pong消息來通知整個(gè)集群對(duì)自身狀態(tài)進(jìn)行更新。

? fail消息:當(dāng)節(jié)點(diǎn)判定集群內(nèi)另一個(gè)節(jié)點(diǎn)下線時(shí),會(huì)向集群內(nèi)廣播一個(gè)fail消息,其他節(jié)點(diǎn)接收到fail消息之后把對(duì)應(yīng)節(jié)點(diǎn)更新為下線狀態(tài)。

消息格式:

所有的消息格式劃分為:消息頭和消息體。

消息頭包含發(fā)送節(jié)點(diǎn)自身狀態(tài)數(shù)據(jù),接收節(jié)點(diǎn)根據(jù)消息頭就可以獲取到發(fā)送節(jié)點(diǎn)的相關(guān)數(shù)據(jù)。



消息格式數(shù)據(jù)結(jié)構(gòu)

消息頭:包含自身的狀態(tài)數(shù)據(jù),發(fā)送節(jié)點(diǎn)關(guān)鍵信息,如節(jié)點(diǎn)id、槽映等節(jié)點(diǎn)標(biāo)識(shí)(主從角色,是否下線)等。


消息頭源碼定義


消息格式數(shù)據(jù)結(jié)構(gòu)

消息體:

定義發(fā)送消息的數(shù)據(jù)。

消息體在Redis內(nèi)部采用clusterMsgData結(jié)構(gòu)聲明:


消息體源碼定義



通信消息處理流程

當(dāng)接收到ping、meet消息時(shí),接收節(jié)點(diǎn)會(huì)解析消息內(nèi)容并根據(jù)自身的識(shí)別情況做出相應(yīng)處理:

接收節(jié)點(diǎn)收到ping/meet消息時(shí),執(zhí)行解析消息頭和

消息體流程:

? 解析消息頭過程:消息頭包含了發(fā)送節(jié)點(diǎn)的信息,如果發(fā)送節(jié)點(diǎn)是新節(jié)點(diǎn)且消息是meet類型,則加入到本地節(jié)點(diǎn)列表;如果是已知節(jié)點(diǎn),則嘗試更新發(fā)送節(jié)點(diǎn)的狀態(tài),如槽映射關(guān)系、主從角色等狀態(tài)。

? 解析消息體過程:如果消息體的clusterMsgDataGossip數(shù)組包含的節(jié)點(diǎn)是新節(jié)點(diǎn),則嘗試發(fā)起與新節(jié)點(diǎn)的meet握手流程;如果是已知節(jié)點(diǎn),則根據(jù)clusterMsgDataGossip中的flags字段判斷該節(jié)點(diǎn)是否下線,用于故障轉(zhuǎn)移。

消息處理完后回復(fù)pong消息,內(nèi)容同樣包含消息頭和消息體,發(fā)送節(jié)點(diǎn)接收到回復(fù)的pong消息后,采用類似的流程解析處理消息并更新與接收節(jié)點(diǎn)最后信息時(shí)間,完成一次消息通信。


通信消息處理流程


通信規(guī)則

Redis集群內(nèi)節(jié)點(diǎn)通信采用固定頻率(定時(shí)任務(wù)每秒執(zhí)行10次)。由于內(nèi)部需要頻繁地進(jìn)行節(jié)點(diǎn)信息交換,而ping/pong消息會(huì)攜帶當(dāng)前節(jié)點(diǎn)和部分其他節(jié)點(diǎn)的狀態(tài)數(shù)據(jù),勢(shì)必會(huì)加重帶寬和計(jì)算的負(fù)擔(dān)。

? 通信節(jié)點(diǎn)選擇過多可以讓信息及時(shí)交換,但是成本過高;

? 通信節(jié)點(diǎn)選擇過少會(huì)降低集群內(nèi)所有節(jié)點(diǎn)彼此信息交換頻率,從而影響故障判定、新節(jié)點(diǎn)發(fā)現(xiàn)等需求的速度。

Redis Cluster 通信規(guī)則


節(jié)點(diǎn)選擇

消息交換的成本主要體現(xiàn)在單位時(shí)間選擇發(fā)送消息的節(jié)點(diǎn)數(shù)量和每個(gè)消息攜帶的數(shù)據(jù)量。

(1)選擇發(fā)送消息的節(jié)點(diǎn)數(shù)量

集群內(nèi)每個(gè)節(jié)點(diǎn)維護(hù)定時(shí)任務(wù)默認(rèn)每秒執(zhí)行10次,每秒會(huì)隨機(jī)選擇5個(gè)節(jié)點(diǎn)找出最久沒有通信的節(jié)點(diǎn)發(fā)送ping消息,用于Gossip信息交換的隨機(jī)性。每100毫秒都會(huì)掃描本地節(jié)點(diǎn)列表,如果發(fā)現(xiàn)節(jié)點(diǎn)最后一次接受pong消息的時(shí)間大于cluster_node_timeout/2,則立刻發(fā)送ping消息,防止該節(jié)點(diǎn)信息太長時(shí)間未更新。根據(jù)以上規(guī)則得出每個(gè)節(jié)點(diǎn)每秒需要發(fā)送ping消息的數(shù)量=1+10*num(node.pong_received> cluster_node_timeout/2),因此cluster_node_timeout參數(shù)對(duì)消息發(fā)送的節(jié)點(diǎn)數(shù)量影響非常大。當(dāng)我們的帶寬資源緊張時(shí),可以適當(dāng)調(diào)大此參數(shù)。但是如果cluster_node_timeout過大會(huì)影響消息交換的頻率從而影響故障轉(zhuǎn)移、槽信息更新、新節(jié)點(diǎn)發(fā)現(xiàn)的速度。因此需要根據(jù)業(yè)務(wù)容忍度和資源消耗進(jìn)行平衡。同時(shí)整個(gè)集群消息總交換量也跟節(jié)點(diǎn)數(shù)成正比。

(2)消息數(shù)據(jù)量

每個(gè)ping消息的數(shù)據(jù)量體現(xiàn)在消息頭和消息體中,其中消息頭主要占用空間的字段是myslots[CLUSTER_SLOTS/8],占用2KB,這塊空間占用相對(duì)固定。消息體會(huì)攜帶一定數(shù)量的其他節(jié)點(diǎn)信息用于信息交換。而消息體攜帶數(shù)據(jù)量跟集群的節(jié)點(diǎn)數(shù)量相關(guān),集群越大每次消息通信的成本也就更高。


通信開銷

Redis Cluster內(nèi)節(jié)點(diǎn)通信自身開銷:

(1)節(jié)點(diǎn)自身信息,主要是自己負(fù)責(zé)的slots信息:slots[CLUSTER_SLOTS/8],占用2KB;

(2)攜帶總節(jié)點(diǎn)1/10的其他節(jié)點(diǎn)的狀態(tài)信息(1個(gè)節(jié)點(diǎn)的狀態(tài)數(shù)據(jù)約為104byte)

注:并不是所有的都是攜帶十分之一的節(jié)點(diǎn)信息的。

如果total_nodes/10小于3,那就至少攜帶3個(gè)節(jié)點(diǎn)信息;

如果total_nodes/10大于total_nodes-2,最多攜帶total_nodes-2個(gè)節(jié)點(diǎn)信息;

Else就total_nodes/10個(gè)節(jié)點(diǎn)信息。


Gossip 通信攜帶其他節(jié)點(diǎn)數(shù)量判斷源碼



通信開銷

節(jié)點(diǎn)狀態(tài)信息:clusterMsgDataGossip,ping、meet、pong采用clusterMsgDataGossip數(shù)組作為消息體。

節(jié)點(diǎn)狀態(tài)信息定義

所以每個(gè)Gossip消息大小為2KB+total_nodes/10*104b

Redis Cluster帶寬消耗主要為:業(yè)務(wù)操作(讀寫)消耗+Gossip消息消耗。


我們現(xiàn)在假設(shè)節(jié)點(diǎn)數(shù)為64*2=128,floor(122)=12:

每個(gè)Gossip消息的大小約為:2KB+12*104b ≈ 3KB。

根據(jù)之前的每個(gè)節(jié)點(diǎn)每秒需要發(fā)送ping消息的數(shù)量=1+10*num(node.pong_received> cluster_node_timeout/2)

假設(shè):cluster_node_timeout為15秒時(shí),num=20,即開銷=3KB*(1+10*20)*2*20=25MB/s;

cluster_node_timeout為30秒時(shí),num=5,即開銷=3KB*(1+10*5)*2*20=6MB/s。

可以看出影響Gossip開銷的主要兩點(diǎn):Cluster Redis的節(jié)點(diǎn)數(shù)和cluster_node_timeout設(shè)置的閾值:

那如果節(jié)點(diǎn)越多,Gossip消息就越大,最近接收pong消息時(shí)間間隔大于cluster_node_timeout/2秒的節(jié)點(diǎn)也會(huì)越多,那么帶寬的開銷越大。

所以得出如下結(jié)論:

(1)盡量避免大集群,針對(duì)大集群就拆分出去;

(2)如果某些場(chǎng)景必須使用大集群,那就可以通過增大cluster_node_timeout來降低帶寬的消耗,但是會(huì)影響failover的時(shí)效,這個(gè)可以根據(jù)業(yè)務(wù)場(chǎng)景和集群具體狀態(tài)評(píng)估;

(3)docker的分配問題,將大集群打散到小集群的物理機(jī)上,可以平衡和更高效的利用資源。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Redis Cluster原理分析 文章較長,如需轉(zhuǎn)載可分段。轉(zhuǎn)載請(qǐng)標(biāo)明作者以及文章來源,謝謝! 作者介紹 姓名:...
    lihanglucien閱讀 20,554評(píng)論 3 30
  • 基本目標(biāo)與設(shè)計(jì)基本思想 Redis cluster 目標(biāo) 高性能,并且能線性擴(kuò)展到1000個(gè)節(jié)點(diǎn)。不需要代理,使用...
    tafeng閱讀 2,805評(píng)論 0 0
  • 本文將從設(shè)計(jì)思路,功能實(shí)現(xiàn),源碼幾個(gè)方面介紹Redis Cluster。假設(shè)讀者已經(jīng)了解Redis Cluster...
    CatKang閱讀 1,485評(píng)論 0 2
  • 轉(zhuǎn)發(fā):Redis Cluster探索與思考 Redis Cluster的基本原理和架構(gòu) Redis Cluster...
    meng_philip123閱讀 3,617評(píng)論 0 14
  • Redis Cluster介紹 redis cluster是Redis的分布式解決方案,在3.0版本推出后有效地解...
    dayspring閱讀 11,044評(píng)論 0 3