Redis——集群(cluster)


 

前言

在前面的文章中,已經介紹了Redis的幾種高可用技術:持久化、主從復制和哨兵,但這些方案仍有不足,其中最主要的問題是存儲能力受單機限制,以及無法實現寫操作的負載均衡。

Redis集群解決了上述問題,實現了較為完善的高可用方案。本文將詳細介紹集群。

主要內容包括:集群的作用;集群的搭建方法及設計方案;集群的基本原理;客戶端訪問集群的方法;以及其他實踐中需要的集群知識(集群擴容、故障轉移、參數優化等)

 

一、集群的作用

集群,即Redis Cluster,是Redis 3.0開始引入的分布式存儲方案。

集群由多個節點(Node)組成,Redis的數據分布在這些節點中。集群中的節點分為主節點和從節點:只有主節點負責讀寫請求和集群信息的維護;從節點只進行主節點數據和狀態信息的復制。

集群的作用,可以歸納為兩點:

1、數據分區:數據分區(或稱數據分片)是集群最核心的功能。

集群將數據分散到多個節點,一方面突破了Redis單機內存大小的限制,存儲容量大大增加;另一方面每個主節點都可以對外提供讀服務和寫服務,大大提高了集群的響應能力。

Redis單機內存大小受限問題,在介紹持久化和主從復制時都有提及;例如,如果單機內存太大,bgsave和bgrewriteaof的fork操作可能導致主進程阻塞,

主從環境下主機切換時可能導致從節點長時間無法提供服務,全量復制階段主節點的復制緩沖區可能溢出……。

2、高可用:集群支持主從復制和主節點的自動故障轉移(與哨兵類似);當任一節點發生故障時,集群仍然可以對外提供服務。

本文內容基於Redis 3.0.6。

 

二、集群的搭建

這一部分我們將搭建一個簡單的集群:共6個節點,3主3從。方便起見:所有節點在同一台服務器上,以端口號進行區分;

配置從簡。3個主節點端口號:7000/7001/7002,對應的從節點端口號:8000/8001/8002。

集群的搭建有兩種方式:

  • (1)手動執行Redis命令,一步步完成搭建;
  • (2)使用Ruby腳本搭建。

二者搭建的原理是一樣的,只是Ruby腳本將Redis命令進行了打包封裝;在實際應用中推薦使用腳本方式,簡單快捷不容易出錯。下面分別介紹這兩種方式。

1. 執行Redis命令搭建集群

集群的搭建可以分為四步:

  • (1)啟動節點:將節點以集群模式啟動,此時節點是獨立的,並沒有建立聯系;
  • (2)節點握手:讓獨立的節點連成一個網絡;
  • (3)分配槽:將16384個槽分配給主節點;
  • (4)指定主從關系:為從節點指定主節點。

實際上,前三步完成后集群便可以對外提供服務;但指定從節點后,集群才能夠提供真正高可用的服務。

  • (1)啟動節點

集群節點的啟動仍然是使用redis-server命令,但需要使用集群模式啟動。下面是7000節點的配置文件(只列出了節點正常工作關鍵配置,其他配置(如開啟AOF)可以參照單機節點進行):

#redis-7000.conf
port 7000
cluster-enabled yes
cluster-config-file "node-7000.conf"
logfile "log-7000.log"
dbfilename "dump-7000.rdb"
daemonize yes

其中的cluster-enabled和cluster-config-file是與集群相關的配置。

cluster-enabled yesRedis實例可以分為單機模式(standalone)和集群模式(cluster);cluster-enabled yes可以啟動集群模式。

在單機模式下啟動的Redis實例,如果執行info server命令,可以發現redis_mode一項為standalone,如下圖所示:

集群模式下的節點,其redis_mode為cluster,如下圖所示:

cluster-config-file該參數指定了集群配置文件的位置。每個節點在運行過程中,會維護一份集群配置文件;每當集群信息發生變化時(如增減節點),

集群內所有節點會將最新信息更新到該配置文件;當節點重啟后,會重新讀取該配置文件,獲取集群信息,可以方便的重新加入到集群中。

也就是說,當Redis節點以集群模式啟動時,會首先尋找是否有集群配置文件,如果有則使用文件中的配置啟動,如果沒有,則初始化配置並將配置保存到文件中。

集群配置文件由Redis節點維護,不需要人工修改。

編輯好配置文件后,使用redis-server命令啟動該節點:

redis-server redis-7000.conf

節點啟動以后,通過cluster nodes命令可以查看節點的情況,如下圖所示。

其中返回值第一項表示節點id,由40個16進制字符串組成,節點id與 主從復制 一文中提到的runId不同:Redis每次啟動runId都會重新創建,

但是節點id只在集群初始化時創建一次,然后保存到集群配置文件中,以后節點重新啟動時會直接在集群配置文件中讀取。

其他節點使用相同辦法啟動,不再贅述。需要特別注意,在啟動節點階段,節點是沒有主從關系的,因此從節點不需要加slaveof配置。

  • (2)節點握手

節點啟動以后是相互獨立的,並不知道其他節點存在;需要進行節點握手,將獨立的節點組成一個網絡。

節點握手使用cluster meet {ip} {port}命令實現,例如在7000節點中執行cluster meet 192.168.72.128 7001,可以完成7000節點和7001節點的握手;

注意ip使用的是局域網ip而不是localhost或127.0.0.1,是為了其他機器上的節點或客戶端也可以訪問。此時再使用cluster nodes查看:

在7001節點下也可以類似查看:

同理,在7000節點中使用cluster meet命令,可以將所有節點加入到集群,完成節點握手:

cluster meet 192.168.72.128 7002
cluster meet 192.168.72.128 8000
cluster meet 192.168.72.128 8001
cluster meet 192.168.72.128 8002

執行完上述命令后,可以看到7000節點已經感知到了所有其他節點:

 

通過節點之間的通信,每個節點都可以感知到所有其他節點,以8000節點為例:

 

  • (3)分配槽

在Redis集群中,借助槽實現數據分區,具體原理后文會介紹。集群有16384個槽,槽是數據管理和遷移的基本單位。

當數據庫中的16384個槽都分配了節點時,集群處於上線狀態(ok);如果有任意一個槽沒有分配節點,則集群處於下線狀態(fail)。

cluster info命令可以查看集群狀態,分配槽之前狀態為fail:

分配槽使用cluster addslots命令,執行下面的命令將槽(編號0-16383)全部分配完畢:

redis-cli -p 7000 cluster addslots {0..5461}
redis-cli -p 7001 cluster addslots {5462..10922}
redis-cli -p 7002 cluster addslots {10923..16383}

此時查看集群狀態,顯示所有槽分配完畢,集群進入上線狀態:

 

  • (4)指定主從關系

集群中指定主從關系不再使用slaveof命令,而是使用cluster replicate命令;參數使用節點id。

通過cluster nodes獲得幾個主節點的節點id后,執行下面的命令為每個從節點指定主節點:

redis-cli -p 8000 cluster replicate be816eba968bc16c884b963d768c945e86ac51ae
redis-cli -p 8001 cluster replicate 788b361563acb175ce8232569347812a12f1fdb4
redis-cli -p 8002 cluster replicate a26f1624a3da3e5197dde267de683d61bb2dcbf1

此時執行cluster nodes查看各個節點的狀態,可以看到主從關系已經建立。

 

 至此,集群搭建完畢。

2. 使用Ruby腳本搭建集群

在{REDIS_HOME}/src目錄下可以看到redis-trib.rb文件,這是一個Ruby腳本,可以實現自動化的集群搭建。

(1)安裝Ruby環境

以Ubuntu為例,如下操作即可安裝Ruby環境:

apt-get install ruby #安裝ruby環境
gem install redis #gem是ruby的包管理工具,該命令可以安裝ruby-redis依賴

(2)啟動節點

與第一種方法中的“啟動節點”完全相同。

(3)搭建集群

redis-trib.rb腳本提供了眾多命令,其中create用於搭建集群,使用方法如下:

./redis-trib.rb create --replicas 1 192.168.72.128:7000 192.168.72.128:7001 192.168.72.128:7002 192.168.72.128:8000 192.168.72.128:8001 192.168.72.128:8002

其中:--replicas=1表示每個主節點有1個從節點;后面的多個{ip:port}表示節點地址,前面的做主節點,后面的做從節點。使用redis-trib.rb搭建集群時,要求節點不能包含任何槽和數據。

執行創建命令后,腳本會給出創建集群的計划,如下圖所示;計划包括哪些是主節點,哪些是從節點,以及如何分配槽。

輸入yes確認執行計划,腳本便開始按照計划執行,如下圖所示。

 

 至此,集群搭建完畢。

3. 集群方案設計

設計集群方案時,至少要考慮以下因素:

(1)高可用要求:根據故障轉移的原理,至少需要3個主節點才能完成故障轉移,且3個主節點不應在同一台物理機上;每個主節點至少需要1個從節點,且主從節點不應在一台物理機上;因此高可用集群至少包含6個節點。

(2)數據量和訪問量:估算應用需要的數據量和總訪問量(考慮業務發展,留有冗余),結合每個主節點的容量和能承受的訪問量(可以通過benchmark得到較准確估計),計算需要的主節點數量。

(3)節點數量限制:Redis官方給出的節點數量限制為1000,主要是考慮節點間通信帶來的消耗。在實際應用中應盡量避免大集群;如果節點數量不足以滿足應用對Redis數據量和訪問量的要求,可以考慮:(1)業務分割,大集群分為多個小集群;(2)減少不必要的數據;(3)調整數據過期策略等。

(4)適度冗余:Redis可以在不影響集群服務的情況下增加節點,因此節點數量適當冗余即可,不用太大。

 

三、集群的基本原理

上一章介紹了集群的搭建方法和設計方案,下面將進一步深入,介紹集群的原理。集群最核心的功能是數據分區,因此首先介紹數據的分區規則;

然后介紹集群實現的細節:通信機制和數據結構;最后以cluster meet(節點握手)、cluster addslots(槽分配)為例,說明節點是如何利用上述數據結構和通信機制實現集群命令的。

1. 數據分區方案

數據分區有順序分區、哈希分區等,其中哈希分區由於其天然的隨機性,使用廣泛;集群的分區方案便是哈希分區的一種。

哈希分區的基本思路是:對數據的特征值(如key)進行哈希,然后根據哈希值決定數據落在哪個節點。常見的哈希分區包括:哈希取余分區、一致性哈希分區、帶虛擬節點的一致性哈希分區等。

衡量數據分區方法好壞的標准有很多,其中比較重要的兩個因素是(1)數據分布是否均勻(2)增加或刪減節點對數據分布的影響。

由於哈希的隨機性,哈希分區基本可以保證數據分布均勻;因此在比較哈希分區方案時,重點要看增減節點對數據分布的影響。

(1)哈希取余分區

哈希取余分區思路非常簡單:計算key的hash值,然后對節點數量進行取余,從而決定數據映射到哪個節點上。

該方案最大的問題是,當新增或刪減節點時,節點數量發生變化,系統中所有的數據都需要重新計算映射關系,引發大規模數據遷移。

(2)一致性哈希分區

一致性哈希算法將整個哈希值空間組織成一個虛擬的圓環,如下圖所示,范圍為0-2^32-1;對於每個數據,根據key計算hash值,

確定數據在環上的位置,然后從此位置沿環順時針行走,找到的第一台服務器就是其應該映射到的服務器。

與哈希取余分區相比,一致性哈希分區將增減節點的影響限制在相鄰節點。以上圖為例,如果在node1和node2之間增加node5,

則只有node2中的一部分數據會遷移到node5;如果去掉node2,則原node2中的數據只會遷移到node4中,只有node4會受影響。

一致性哈希分區的主要問題在於,當節點數量較少時,增加或刪減節點,對單個節點的影響可能很大,造成數據的嚴重不平衡。

還是以上圖為例,如果去掉node2,node4中的數據由總數據的1/4左右變為1/2左右,與其他節點相比負載過高。

(3)帶虛擬節點的一致性哈希分區

該方案在一致性哈希分區的基礎上,引入了虛擬節點的概念。Redis集群使用的便是該方案,其中的虛擬節點稱為槽(slot)。

槽是介於數據和實際節點之間的虛擬概念;每個實際節點包含一定數量的槽,每個槽包含哈希值在一定范圍內的數據。引入槽以后,數據的映射關系由數據hash->實際節點,變成了數據hash->槽->實際節點。

在使用了槽的一致性哈希分區中,槽是數據管理和遷移的基本單位。槽解耦了數據和實際節點之間的關系,增加或刪除節點對系統的影響很小。

仍以上圖為例,系統中有4個實際節點,假設為其分配16個槽(0-15); 槽0-3位於node1,4-7位於node2,以此類推。如果此時刪除node2,只需要將槽4-7重新分配即可,

例如槽4-5分配給node1,槽6分配給node3,槽7分配給node4;可以看出刪除node2后,數據在其他節點的分布仍然較為均衡。

槽的數量一般遠小於2^32,遠大於實際節點的數量;在Redis集群中,槽的數量為16384。

下面這張圖很好的總結了Redis集群將數據映射到實際節點的過程:

(1)Redis對數據的特征值(一般是key)計算哈希值,使用的算法是CRC16。

(2)根據哈希值,計算數據屬於哪個槽。

(3)根據槽與節點的映射關系,計算數據屬於哪個節點。

2. 節點通信機制

集群要作為一個整體工作,離不開節點之間的通信。

  • 兩個端口

在哨兵系統中,節點分為數據節點和哨兵節點:前者存儲數據,后者實現額外的控制功能。在集群中,沒有數據節點與非數據節點之分:

所有的節點都存儲數據,也都參與集群狀態的維護。為此,集群中的每個節點,都提供了兩個TCP端口:

  1. 普通端口:即我們在前面指定的端口(7000等)。普通端口主要用於為客戶端提供服務(與單機節點類似);但在節點間數據遷移時也會使用。
  2. 集群端口:端口號是普通端口+10000(10000是固定值,無法改變),如7000節點的集群端口為17000。集群端口只用於節點之間的通信,如搭建集群、增減節點、故障轉移等操作時節點間的通信;不要使用客戶端連接集群接口。為了保證集群可以正常工作,在配置防火牆時,要同時開啟普通端口和集群端口。
  • Gossip協議

節點間通信,按照通信協議可以分為幾種類型:單對單、廣播、Gossip協議等。重點是廣播和Gossip的對比。

廣播是指向集群內所有節點發送消息;優點是集群的收斂速度快(集群收斂是指集群內所有節點獲得的集群信息是一致的),缺點是每條消息都要發送給所有節點,CPU、帶寬等消耗較大。

Gossip協議的特點是:在節點數量有限的網絡中,每個節點都“隨機”的與部分節點通信(並不是真正的隨機,而是根據特定的規則選擇通信的節點),

經過一番雜亂無章的通信,每個節點的狀態很快會達到一致。Gossip協議的優點有負載(比廣播)低、去中心化、容錯性高(因為通信有冗余)等;缺點主要是集群的收斂速度慢。

  • 消息類型

集群中的節點采用固定頻率(每秒10次)的定時任務進行通信相關的工作:判斷是否需要發送消息及消息類型、確定接收節點、發送消息等。如果集群狀態發生了變化,

如增減節點、槽狀態變更,通過節點間的通信,所有節點會很快得知整個集群的狀態,使集群收斂。

節點間發送的消息主要分為5種:meet消息、ping消息、pong消息、fail消息、publish消息。不同的消息類型,通信協議、發送的頻率和時機、接收節點的選擇等是不同的。

  1. MEET消息:在節點握手階段,當節點收到客戶端的CLUSTER MEET命令時,會向新加入的節點發送MEET消息,請求新節點加入到當前集群;新節點收到MEET消息后會回復一個PONG消息。
  2. PING消息:集群里每個節點每秒鍾會選擇部分節點發送PING消息,接收者收到消息后會回復一個PONG消息。PING消息的內容是自身節點和部分其他節點的狀態信息;作用是彼此交換信息,以及檢測節點是否在線。PING消息使用Gossip協議發送,接收節點的選擇兼顧了收斂速度和帶寬成本,具體規則如下:(1)隨機找5個節點,在其中選擇最久沒有通信的1個節點(2)掃描節點列表,選擇最近一次收到PONG消息時間大於cluster_node_timeout/2的所有節點,防止這些節點長時間未更新。
  3. PONG消息:PONG消息封裝了自身狀態數據。可以分為兩種:第一種是在接到MEET/PING消息后回復的PONG消息;第二種是指節點向集群廣播PONG消息,這樣其他節點可以獲知該節點的最新信息,例如故障恢復后新的主節點會廣播PONG消息。
  4. FAIL消息:當一個主節點判斷另一個主節點進入FAIL狀態時,會向集群廣播這一FAIL消息;接收節點會將這一FAIL消息保存起來,便於后續的判斷。
  5. PUBLISH消息:節點收到PUBLISH命令后,會先執行該命令,然后向集群廣播這一消息,接收節點也會執行該PUBLISH命令。

 

四、客戶端訪問集群

在集群中,數據分布在不同的節點中,客戶端通過某節點訪問數據時,數據可能不在該節點中;下面介紹集群是如何處理這個問題的。

1. redis-cli

當節點收到redis-cli發來的命令(如set/get)時,過程如下:

(1)計算key屬於哪個槽:CRC16(key) & 16383

集群提供的cluster keyslot命令也是使用上述公式實現,如:

(2)判斷key所在的槽是否在當前節點:假設key位於第i個槽,clusterState.slots[i]則指向了槽所在的節點,如果clusterState.slots[i]==clusterState.myself,

         說明槽在當前節點,可以直接在當前節點執行命令;否則,說明槽不在當前節點,則查詢槽所在節點的地址(clusterState.slots[i].ip/port),並將其包裝到MOVED錯誤中返回給redis-cli。

(3)redis-cli收到MOVED錯誤后,根據返回的ip和port重新發送請求。

下面的例子展示了redis-cli和集群的互動過程:在7000節點中操作key1,但key1所在的槽9189在節點7001中,因此節點返回MOVED錯誤(包含7001節點的ip和port)給redis-cli,redis-cli重新向7001發起請求。

上例中,redis-cli通過-c指定了集群模式,如果沒有指定,redis-cli無法處理MOVED錯誤:

 

 

五、實踐須知

前面介紹了集群正常運行和訪問的方法和原理,下面是一些重要的補充內容。

1. 集群伸縮

實踐中常常需要對集群進行伸縮,如訪問量增大時的擴容操作。Redis集群可以在不影響對外服務的情況下實現伸縮;

伸縮的核心是槽遷移:修改槽與節點的對應關系,實現槽(即數據)在節點之間的移動。

例如,如果槽均勻分布在集群的3個節點中,此時增加一個節點,則需要從3個節點中分別拿出一部分槽給新節點,從而實現槽在4個節點中的均勻分布。

  • 增加節點

假設要增加7003和8003節點,其中8003是7003的從節點;步驟如下:

(1)啟動節點:方法參見集群搭建

(2)節點握手:可以使用cluster meet命令,但在生產環境中建議使用redis-trib.rb的add-node工具,其原理也是cluster meet,但它會先檢查新節點是否已加入其它集群或者存在數據,避免加入到集群后帶來混亂。

redis-trib.rb add-node 192.168.72.128:7003 192.168.72.128 7000
redis-trib.rb add-node 192.168.72.128:8003 192.168.72.128 7000

(3)遷移槽:推薦使用redis-trib.rb的reshard工具實現。reshard自動化程度很高,只需要輸入redis-trib.rb reshard ip:port (ip和port可以是集群中的任一節點),然后按照提示輸入以下信息,槽遷移會自動完成:

  • 待遷移的槽數量:16384個槽均分給4個節點,每個節點4096個槽,因此待遷移槽數量為4096
  • 目標節點id:7003節點的id
  • 源節點的id:7000/7001/7002節點的id

(4)指定主從關系:方法參見集群搭建

  • 減少節點

假設要下線7000/8000節點,可以分為兩步:

(1)遷移槽:使用reshard將7000節點中的槽均勻遷移到7001/7002/7003節點

(2)下線節點:使用redis-trib.rb del-node工具;應先下線從節點再下線主節點,因為若主節點先下線,從節點會被指向其他主節點,造成不必要的全量復制。

redis-trib.rb del-node 192.168.72.128:7001 {節點8000的id}
redis-trib.rb del-node 192.168.72.128:7001 {節點7000的id}

2. 故障轉移

在 哨兵 一文中,介紹了哨兵實現故障發現和故障轉移的原理。雖然細節上有很大不同,但集群的實現與哨兵思路類似:通過定時任務發送PING消息檢測其他節點狀態;

節點下線分為主觀下線和客觀下線;客觀下線后選取從節點進行故障轉移。與哨兵一樣,集群只實現了主節點的故障轉移;從節點故障時只會被下線,不會進行故障轉移。

因此,使用集群時,應謹慎使用讀寫分離技術,因為從節點故障會導致讀服務不可用,可用性變差。

這里不再詳細介紹故障轉移的細節,只對重要事項進行說明:

節點數量:在故障轉移階段,需要由主節點投票選出哪個從節點成為新的主節點;從節點選舉勝出需要的票數為N/2+1;

其中N為主節點數量(包括故障主節點),但故障主節點實際上不能投票。因此為了能夠在故障發生時順利選出從節點,集群中至少需要3個主節點(且部署在不同的物理機上)。

故障轉移時間:從主節點故障發生到完成轉移,所需要的時間主要消耗在主觀下線識別、主觀下線傳播、選舉延遲等幾個環節;具體時間與參數cluster-node-timeout有關,一般來說:

故障轉移時間(毫秒) ≤ 1.5 * cluster-node-timeout + 1000

cluster-node-timeout的默認值為15000ms(15s),因此故障轉移時間會在20s量級。

3. 集群的限制及應對方法

由於集群中的數據分布在不同節點中,導致一些功能受限,包括:

(1)key批量操作受限:例如mget、mset操作,只有當操作的key都位於一個槽時,才能進行。針對該問題,一種思路是在客戶端記錄槽與key的信息,每次針對特定槽執行mget/mset;另外一種思路是使用Hash Tag,將在下一小節介紹。

(2)keys/flushall等操作:keys/flushall等操作可以在任一節點執行,但是結果只針對當前節點,例如keys操作只返回當前節點的所有鍵。針對該問題,可以在客戶端使用cluster nodes獲取所有節點信息,並對其中的所有主節點執行keys/flushall等操作。

(3)事務/Lua腳本:集群支持事務及Lua腳本,但前提條件是所涉及的key必須在同一個節點。Hash Tag可以解決該問題。

(4)數據庫:單機Redis節點可以支持16個數據庫,集群模式下只支持一個,即db0。

(5)復制結構:只支持一層復制結構,不支持嵌套。

4. Hash Tag

Hash Tag原理是:當一個key包含 {} 的時候,不對整個key做hash,而僅對 {} 包括的字符串做hash

Hash Tag可以讓不同的key擁有相同的hash值,從而分配在同一個槽里;這樣針對不同key的批量操作(mget/mset等),以及事務、Lua腳本等都可以支持。

不過Hash Tag可能會帶來數據分配不均的問題,這時需要:(1)調整不同節點中槽的數量,使數據分布盡量均勻;(2)避免對熱點數據使用Hash Tag,導致請求分布不均。

下面是使用Hash Tag的一個例子;通過對product加Hash Tag,可以將所有產品信息放到同一個槽中,便於操作。

5. 參數優化

  • cluster_node_timeout

cluster_node_timeout參數在前面已經初步介紹;它的默認值是15s,影響包括:

(1)影響PING消息接收節點的選擇:值越大對延遲容忍度越高,選擇的接收節點越少,可以降低帶寬,但會降低收斂速度;應根據帶寬情況和應用要求進行調整。

(2)影響故障轉移的判定和時間:值越大,越不容易誤判,但完成轉移消耗時間越長;應根據網絡狀況和應用要求進行調整。

  • cluster-require-full-coverage

前面提到,只有當16384個槽全部分配完畢時,集群才能上線。這樣做是為了保證集群的完整性,但同時也帶來了新的問題:

當主節點發生故障而故障轉移尚未完成,原主節點中的槽不在任何節點中,此時會集群處於下線狀態,無法響應客戶端的請求。

cluster-require-full-coverage參數可以改變這一設定:如果設置為no,則當槽沒有完全分配時,集群仍可以上線。

參數默認值為yes,如果應用對可用性要求較高,可以修改為no,但需要自己保證槽全部分配。

6. redis-trib.rb

redis-trib.rb提供了眾多實用工具:創建集群、增減節點、槽遷移、檢查完整性、數據重新平衡等;通過help命令可以查看詳細信息。

在實踐中如果能使用redis-trib.rb工具則盡量使用,不但方便快捷,還可以大大降低出錯概率。

 

 

轉載:https://www.cnblogs.com/kismetv/p/9853040.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM