018.Redis Cluster故障轉(zhuǎn)移原理

1. 故障發(fā)現(xiàn)

當(dāng)集群內(nèi)某個節(jié)點出現(xiàn)問題時,需要通過一種健壯的方式保證識別出節(jié)點是否發(fā)生了故障。Redis集群內(nèi)節(jié)點通過ping/pong消息實現(xiàn)節(jié)點通信,消息不但可以傳播節(jié)點槽信息,還可以傳播其他狀態(tài)如:主從狀態(tài)、節(jié)點故障等。因此故障發(fā)現(xiàn)也是通過消息傳播機(jī)制實現(xiàn)的,主要環(huán)節(jié)包括:主觀下線(PFAIL-Possibly Fail)客觀下線(Fail)

  • 主觀下線:指某個節(jié)點認(rèn)為另一個節(jié)點不可用,即下線狀態(tài),這個狀態(tài)并不是最終的故障判定,只能代表一個節(jié)點的意見,可能存在誤判情況。
  • 客觀下線:指標(biāo)記一個節(jié)點真正的下線,集群內(nèi)多個節(jié)點都認(rèn)為該節(jié)點不可用,從而達(dá)成共識的結(jié)果。如果是持有槽的主節(jié)點故障,需要為該節(jié)點進(jìn)行故障轉(zhuǎn)移。

一個節(jié)點認(rèn)為某個節(jié)點失聯(lián)了并不代表所有的節(jié)點都認(rèn)為它失聯(lián)了。所以集群還得經(jīng)過一次協(xié)商的過程,只有當(dāng)大多數(shù)節(jié)點都認(rèn)定了某個節(jié)點失聯(lián)了,集群才認(rèn)為該節(jié)點需要進(jìn)行主從切換來容錯。Redis 集群節(jié)點采用 Gossip 協(xié)議來廣播自己的狀態(tài)以及自己對整個集群認(rèn)知的改變。比如一個節(jié)點發(fā)現(xiàn)某個節(jié)點失聯(lián)了(PFail),它會將這條信息向整個集群廣播,其它節(jié)點也就可以收到這點失聯(lián)信息。如果一個節(jié)點收到了某個節(jié)點失聯(lián)的數(shù)量 (PFail Count) 已經(jīng)達(dá)到了集群的大多數(shù),就可以標(biāo)記該節(jié)點為確定下線狀態(tài) (Fail),然后向整個集群廣播,強(qiáng)迫其它節(jié)點也接收該節(jié)點已經(jīng)下線的事實,并立即對該失聯(lián)節(jié)點進(jìn)行主從切換。

1.1 主觀下線

集群中每個節(jié)點都會定期向其他節(jié)點發(fā)送ping消息,接收節(jié)點回復(fù)pong消息作為響應(yīng)。如果在cluster-node-timeout時間內(nèi)通信一直失敗,則發(fā)送節(jié)點會認(rèn)為接收節(jié)點存在故障,把接收節(jié)點標(biāo)記為主觀下線(PFail)狀態(tài)

  • 節(jié)點a發(fā)送ping消息給節(jié)點b,如果通信正常將接收到pong消息,節(jié)點a更新最近一次與節(jié)點b的通信時間。

  • 如果節(jié)點a與節(jié)點b通信出現(xiàn)問題則斷開連接,下次會進(jìn)行重連。如果一直通信失敗,則節(jié)點a記錄的與節(jié)點b最后通信時間將無法更新。

  • 節(jié)點a內(nèi)的定時任務(wù)檢測到與節(jié)點b最后通信時間超過cluster-node-timeout時,更新本地對節(jié)點b的狀態(tài)為主觀下線(pfail)。

主觀下線簡單來講就是,當(dāng)cluster-note-timeout時間內(nèi)某節(jié)點無法與另一個節(jié)點順利完成ping消息通信時,則將該節(jié)點標(biāo)記為主觀下線狀態(tài)

1.2 客觀下線

Redis集群對于節(jié)點最終是否故障判斷非常嚴(yán)謹(jǐn),只有一個節(jié)點認(rèn)為主觀下線并不能準(zhǔn)確判斷是否故障。當(dāng)某個節(jié)點判斷另一個節(jié)點主觀下線后,相應(yīng)的節(jié)點狀態(tài)會跟隨消息在集群內(nèi)傳播,通過Gossip消息傳播,集群內(nèi)節(jié)點不斷收集到故障節(jié)點的下線報告。當(dāng)半數(shù)以上持有槽的主節(jié)點都標(biāo)記某個節(jié)點是主觀下線時。觸發(fā)客觀下線流
程。

為什么必須是負(fù)責(zé)槽的主節(jié)點參與故障發(fā)現(xiàn)決策?

因為集群模式下只有處理槽的主節(jié)點才負(fù)責(zé)讀寫請求和集群槽等關(guān)鍵信息維護(hù),而從節(jié)點只進(jìn)行主節(jié)點數(shù)據(jù)和狀態(tài)信息的復(fù)制。

為什么半數(shù)以上處理槽的主節(jié)點?

必須半數(shù)以上是為了應(yīng)對網(wǎng)絡(luò)分區(qū)等原因造成的集群分割情況,被分割的小集群因為無法完成從主觀下線到
客觀下線這一關(guān)鍵過程,從而防止小集群完成故障轉(zhuǎn)移之后繼續(xù)對外提供服務(wù)。

客觀下線流程:

  • 當(dāng)消息體內(nèi)含有其他節(jié)點的pfail狀態(tài)會判斷發(fā)送節(jié)點的狀態(tài),如果發(fā)送節(jié)點是主節(jié)點則對報告的pfail狀態(tài)處理,從節(jié)點則忽略。
  • 找到pfail對應(yīng)的節(jié)點,更新其內(nèi)部下線報告(其中記錄了每個節(jié)點對該節(jié)點做出的下線判斷)
  • 根據(jù)更新后的下線報告鏈表告嘗試進(jìn)行客觀下線
  • 每個節(jié)點都維護(hù)一個都下線報告,保存了其他主節(jié)點針對當(dāng)前節(jié)點的下線報告
  • 下線報告中保存了報告故障的節(jié)點和最近收到下線報告的時間
  • 每個下線報告都存在有效期,每次在嘗試觸發(fā)客觀下線時,都會檢測下線報告是否過期,對于過期的下線報告將被刪除。如果在cluster-node-time*2的時間內(nèi)該下線報告沒有得到更新則過期并刪除
  • 下線報告的有效期限是cluster_node_timeout*2,主要是針對故障誤報的情況。例如節(jié)點A在上一小時報告節(jié)點B主觀下線,但是之后又恢復(fù)正常?,F(xiàn)在又有其他節(jié)點上報節(jié)點B主觀下線,根據(jù)實際情況之前的屬于誤
    報不能被使用
  • 統(tǒng)計有效的下線報告數(shù)量,如果小于集群內(nèi)持有槽的主節(jié)點總數(shù)的一半則退出。
  • 當(dāng)下線報告大于槽主節(jié)點數(shù)量一半時,標(biāo)記對應(yīng)故障節(jié)點為客觀下線狀態(tài)。
  • 向集群廣播一條fail消息,通知所有的節(jié)點將故障節(jié)點標(biāo)記為客觀下線,fail消息的消息體只包含故障節(jié)點的ID

注意:

如果在cluster-node-time*2時間內(nèi)無法收集到一半以上槽節(jié)點的下線報告,那么之前的下線報告將會過期,也就是說主觀下線上報的速度追趕不上下線報告過期的速度,那么故障節(jié)點將永遠(yuǎn)無法被標(biāo)記為客觀下線從而導(dǎo)致
故障轉(zhuǎn)移失敗。因此不建議將cluster-node-time設(shè)置得過小

廣播fail消息是客觀下線的最后一步,它承擔(dān)著非常重要的職責(zé):

  • 通知集群內(nèi)所有的節(jié)點標(biāo)記故障節(jié)點為客觀下線狀態(tài)并立刻生效。

  • 通知故障節(jié)點的從節(jié)點觸發(fā)故障轉(zhuǎn)移流程。

需要理解的是,盡管存在廣播fail消息機(jī)制,但是集群所有節(jié)點知道故障節(jié)點進(jìn)入客觀下線狀態(tài)是不確定的。比如當(dāng)出現(xiàn)網(wǎng)絡(luò)分區(qū)時有可能集群被分割為一大一小兩個獨立集群中。大的集群持有半數(shù)槽節(jié)點可以完成客觀下線并廣播fail消息,但是小集群無法接收到fail消息,網(wǎng)絡(luò)分區(qū)會導(dǎo)致分割后的小集群無法收到大集群的fail消息,因此如果故障節(jié)點所有的從節(jié)點都在小集群內(nèi)將導(dǎo)致無法完成后續(xù)故障轉(zhuǎn)移,因此部署主從結(jié)構(gòu)時需要根據(jù)自身機(jī)房/機(jī)架拓?fù)浣Y(jié)構(gòu),降低主從被分區(qū)的可能性。

2. 故障恢復(fù)

故障節(jié)點變?yōu)榭陀^下線后,如果下線節(jié)點是持有槽的主節(jié)點則需要在它的從節(jié)點中選出一個替換它,從而保證集群的高可用。下線主節(jié)點的所有從節(jié)點承擔(dān)故障恢復(fù)的義務(wù),當(dāng)從節(jié)點通過內(nèi)部定時任務(wù)發(fā)現(xiàn)自身復(fù)制的主節(jié)點進(jìn)入客觀下線時,將會觸發(fā)故障恢復(fù)流程

  • 每個從節(jié)點都要檢查最后與主節(jié)點斷線時間,判斷是否有資格替換故障的主節(jié)點。如果從節(jié)點與主節(jié)點斷線時間超過cluster-node-time*cluster-slave-validity-factor,則當(dāng)前從節(jié)點不具備故障轉(zhuǎn)移資格,cluster-slave-validity-factor設(shè)置為0代表任何slave都可以被轉(zhuǎn)換為master,默認(rèn)為10

  • 當(dāng)從節(jié)點符合故障轉(zhuǎn)移資格后,更新觸發(fā)故障選舉的時間,只有到達(dá)該時間后才能執(zhí)行后續(xù)流程,這里之所以采用延遲觸發(fā)機(jī)制,主要是通過對多個從節(jié)點使用不同的延遲選舉時間來支持優(yōu)先級問題。復(fù)制偏移量越大說明從節(jié)點延遲越低,那么它應(yīng)該具有更高的優(yōu)先級來替換故障主節(jié)點,所有的從節(jié)點中復(fù)制偏移量最大的將提前觸發(fā)故障選舉流程

  • 當(dāng)從節(jié)點定時任務(wù)檢測到達(dá)故障選舉時間(failover_auth_time)到達(dá)后,發(fā)起選舉流程

    • 更新配置版本

      配置紀(jì)元是一個只增不減的整數(shù),每個主節(jié)點自身維護(hù)一個配置版本(clusterNode.configEpoch)標(biāo)示當(dāng)前主節(jié)點的版本,所有主節(jié)點的配置版本都不相等,從節(jié)點會復(fù)制主節(jié)點的配置版本。整個集群又維護(hù)一個全局的配置版本(clusterState.current Epoch),用于記錄集群內(nèi)所有主節(jié)點配置版本的最大版本。執(zhí)行cluster info命令可以查看配置版本信息

      10.0.0.102:6379> cluster info
      cluster_current_epoch:6
      cluster_my_epoch:4
      

      配置版本會跟隨ping/pong消息在集群內(nèi)傳播,當(dāng)發(fā)送方與接收方都是主節(jié)點且配置版本相等時代表出現(xiàn)了沖突,nodeId更大的一方會遞增全局配置版本并賦值給當(dāng)前節(jié)點來區(qū)分沖突

      配置版本的主要作用:

      • 標(biāo)示集群內(nèi)每個主節(jié)點的不同版本和當(dāng)前集群最大的版本
      • 每次集群發(fā)生重要事件時,這里的重要事件指出現(xiàn)新的主節(jié)點(新加入的或者由從節(jié)點轉(zhuǎn)換而來),從節(jié)點競爭選舉。都會遞增集群全局的配置版本并賦值給相關(guān)主節(jié)點,用于記錄這一關(guān)鍵事件。
      • 主節(jié)點具有更大的配置版本代表了更新的集群狀態(tài),因此當(dāng)節(jié)點間進(jìn)行ping/pong消息交換時,如出現(xiàn)slots等關(guān)鍵信息不一致時,以配置版本更大的一方為準(zhǔn),防止過時的消息狀態(tài)污染集群。
      • 配置版本的應(yīng)用場景有:
        新節(jié)點加入
        槽節(jié)點映射沖突檢測
        從節(jié)點投票選舉沖突檢測
      • 在通過cluster setslot命令修改槽節(jié)點映射時,需要確保執(zhí)行請求的主節(jié)點本地配置版本是最大值,否則修改后的槽信息在消息傳播中不會被擁有更高的配置版本的節(jié)點采納。由于Gossip通信機(jī)制無法準(zhǔn)確知道當(dāng)前最大的配置版本在哪個節(jié)點,因此在槽遷移任務(wù)最后的cluster setslot {slot} node {nodeId}命令需要在全部主節(jié)點中執(zhí)行一遍。
      • 從節(jié)點每次發(fā)起投票時都會自增集群的全局配置版本,并單獨保存clusterState.failover_auth_epoch變量中用于標(biāo)識本次從節(jié)點發(fā)起選舉的版本
    • 廣播選舉消息

      在集群內(nèi)廣播選舉消息FAILOVER_AUTH_REQUEST,并記錄已發(fā)送過消息的狀態(tài),保證該從節(jié)點在一個配置版本內(nèi)只能發(fā)起一次選舉

  • 選舉投票

    只有持有槽的主節(jié)點才會處理故障選舉消息FAILOVER_AUTH_REQUEST,因為每個持有槽的節(jié)點在一個配置版本內(nèi)都有唯一的一張選票,當(dāng)接到第一個請求投票的從節(jié)點消息時回復(fù)FAILOVER_AUTH_ACK消息作為投票,之后相同配置版本內(nèi)其他從節(jié)點的選舉消息將忽略

    投票過程其實是一個領(lǐng)導(dǎo)者選舉的過程,如集群內(nèi)有N個持有槽的主節(jié)點代表有N張選票。由于在每個配置版本內(nèi)持有槽的主節(jié)點只能投票給一個從節(jié)點,因此只能有一個從節(jié)點獲得N/2+1的選票,保證能夠找出唯一的從節(jié)點。

    Redis集群沒有直接使用從節(jié)點進(jìn)行領(lǐng)導(dǎo)者選舉(投票讓支持槽節(jié)點的master來做,而不是多個slave之間的投票),主要因為從節(jié)點數(shù)必須大于等于3個才能保證湊夠N/2+1個節(jié)點,將導(dǎo)致從節(jié)點資源浪費。使用集群內(nèi)所有持有槽的主節(jié)點進(jìn)行領(lǐng)導(dǎo)者選舉,即使只有一個從節(jié)點也可以完成選舉過程。

    當(dāng)從節(jié)點收集到N/2+1個持有槽的主節(jié)點投票時,從節(jié)點可以執(zhí)行替換主節(jié)點操作,例如集群內(nèi)有5個持有槽的主節(jié)點,主節(jié)點b故障后還有4個,當(dāng)其中一個從節(jié)點收集到3張投票時代表獲得了足夠的選票可以進(jìn)行替換主節(jié)點操作

    故障主節(jié)點也算在投票數(shù)內(nèi),假設(shè)集群內(nèi)節(jié)點規(guī)模是3主3從,其中有2個主節(jié)點部署在一臺機(jī)器上,當(dāng)這臺機(jī)器宕機(jī)時,由于從節(jié)點無法收集到3/2+1個主節(jié)點選票將導(dǎo)致故障轉(zhuǎn)移失敗。這個問題也適用于故障發(fā)現(xiàn)環(huán)節(jié)。因此部署集群時所有主節(jié)點最少需要部署在3臺物理機(jī)上才能避免單點問題。

    投票作廢:每個配置版本代表了一次選舉周期,如果在開始投票之后的cluster-node-timeout*2時間內(nèi)從節(jié)點沒有獲取足夠數(shù)量的投票,則本次選舉作廢。其他從節(jié)點對配置版本自增并發(fā)起下一輪投票,直到選舉成功為止

    Redis Cluster 可以為每個主節(jié)點設(shè)置若干個從節(jié)點,單主節(jié)點故障時,集群會自動將其中某個從節(jié)點提升為主節(jié)點。如果某個主節(jié)點沒有從節(jié)點,那么當(dāng)它發(fā)生故障時,集群將完全處于不可用狀態(tài)。不過 Redis 也提供了一個參數(shù)cluster-require-full-coverage(默認(rèn)yes) 可以允許部分節(jié)點故障,其它節(jié)點還可以繼續(xù)提供對外訪問。

  • 替換主節(jié)點

    當(dāng)從節(jié)點收集到足夠的選票之后,觸發(fā)替換主節(jié)點操作:

    • 當(dāng)前從節(jié)點取消復(fù)制變?yōu)橹鞴?jié)點。
    • 執(zhí)行clusterDelSlot操作撤銷故障主節(jié)點負(fù)責(zé)的槽,并執(zhí)行clusterAddSlot把這些槽委派給自己
    • 向集群廣播自己的pong消息,通知集群內(nèi)所有的節(jié)點當(dāng)前從節(jié)點變?yōu)橹鞴?jié)點并接管了故障主節(jié)點的槽信息。

3. 故障轉(zhuǎn)移時間

  • 主觀下線(pfail)識別時間 = cluster-node-timeout

  • 主觀下線狀態(tài)消息傳播時間 <= cluster-node-timeout/2,消息通信機(jī)制對超過cluster-node-timeout/2未通信節(jié)點會發(fā)起ping消息,消息體在選擇包含哪些節(jié)點時會優(yōu)先選取下線狀態(tài)節(jié)點,所以通常這段時間內(nèi)能夠收集到半數(shù)以上主節(jié)點的pfail報告從而完成故障發(fā)現(xiàn)

  • 從節(jié)點轉(zhuǎn)移時間<=1000毫秒,由于存在延遲發(fā)起選舉機(jī)制,偏移量最大的從節(jié)點會最多延遲1秒發(fā)起選舉。通常第一次選舉就會成功,所以從節(jié)點執(zhí)行轉(zhuǎn)移時間在1秒以內(nèi)

  • 根據(jù)以上分析可以預(yù)估出故障轉(zhuǎn)移時間:failover-time ≤ (cluster-node-timeout * 1.5 + 1000)ms,因此,故障轉(zhuǎn)移時間跟cluster-node-timeout參數(shù)息息相關(guān),默認(rèn)15秒,配置時可以根據(jù)業(yè)務(wù)容忍度做出適當(dāng)調(diào)整,但不是越小越好

4. 故障轉(zhuǎn)移演練

  • 一個master下線
root       3423      1  0 11:38 ?        00:01:06 bin/redis-server 10.0.0.100:6379 [cluster]
root       3428      1  0 11:38 ?        00:01:05 bin/redis-server 10.0.0.100:6380 [cluster]
root       3840   3004  0 17:09 pts/0    00:00:00 grep --color=auto redis
[root@node01 redis]# kill -9 3423
  • slave與下線master的主從復(fù)制中斷
[root@node03 redis]# cat /var/log/redis/redis_6380.log

654:S 25 Mar 17:10:29.783 # Connection with master lost.
2654:S 25 Mar 17:10:29.784 * Caching the disconnected master state.
2654:S 25 Mar 17:10:29.784 * Connecting to MASTER 10.0.0.100:6379
2654:S 25 Mar 17:10:29.784 * MASTER <-> SLAVE sync started
2654:S 25 Mar 17:10:29.785 # Error condition on socket for SYNC: Connection refused
  • 其他兩個master標(biāo)記下線master主觀下線
[root@node02 redis]# cat /var/log/redis/redis_6379.log
2876:M 25 Mar 17:10:45.391 * Marking node 9c02aef2d45e44678202721ac923c615dd8300ea as failing (quorum reached).

[root@node03 redis]# cat /var/log/redis/redis_6379.log
2649:M 25 Mar 17:10:45.411 * Marking node 9c02aef2d45e44678202721ac923c615dd8300ea as failing (quorum reached).
  • 超半數(shù)master認(rèn)為下線master主觀下線,所以下線master客觀下線
  • slave節(jié)點在延遲724ms后,開始準(zhǔn)備選舉,它和下線master的復(fù)制偏移量是21930
2654:S 25 Mar 17:10:45.415 # Cluster state changed: fail
2654:S 25 Mar 17:10:45.510 # Start of election delayed for 724 milliseconds (rank #0, offset 21930).
  • slave更新配置版本并發(fā)起選舉
2654:S 25 Mar 17:10:46.322 # Starting a failover election for epoch 7.
  • 其他兩個master對slave進(jìn)行了投票
2649:M 25 Mar 17:10:46.327 # Failover auth granted to 0955dc1eeeec59c1e9b72eca5bcbcd04af108820 for epoch 7
2876:M 25 Mar 17:10:46.310 # Failover auth granted to 0955dc1eeeec59c1e9b72eca5bcbcd04af108820 for epoch 7
  • 重啟下線的master
[root@node01 redis]# bin/redis-server conf/redis_6379.conf
  • 舊master節(jié)點啟動后發(fā)現(xiàn)自己負(fù)責(zé)的槽指派給另一個節(jié)點,則以現(xiàn)有集群配置為準(zhǔn),變?yōu)樾轮鞴?jié)點的從節(jié)點
3873:M 25 Mar 17:24:32.823 * Node configuration loaded, I'm 9c02aef2d45e44678202721ac923c615dd8300ea
873:M 25 Mar 17:24:32.825 # Configuration change detected. Reconfiguring myself as a replica of 0955dc1eeeec59c1e9b72eca5bcbcd04af108820
3873:S 25 Mar 17:24:32.825 * Before turning into a slave, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
  • 集群內(nèi)其他節(jié)點接收到新上線發(fā)來的ping消息,清空客觀下線狀態(tài)
3428:S 25 Mar 17:24:32.830 * Clear FAIL state for node 9c02aef2d45e44678202721ac923c615dd8300ea: master without slots is reachable again.
2876:M 25 Mar 17:24:32.914 * Clear FAIL state for node 9c02aef2d45e44678202721ac923c615dd8300ea: master without slots is reachable again.
2881:S 25 Mar 17:24:32.916 * Clear FAIL state for node 9c02aef2d45e44678202721ac923c615dd8300ea: master without slots is reachable again.
2654:M 25 Mar 17:24:32.853 * Clear FAIL state for node 9c02aef2d45e44678202721ac923c615dd8300ea: master without slots is reachable again.
2649:M 25 Mar 17:24:32.854 * Clear FAIL state for node 9c02aef2d45e44678202721ac923c615dd8300ea: master without slots is reachable again.
  • 新的主從開始復(fù)制
# slave
3873:S 25 Mar 17:24:33.832 * Connecting to MASTER 10.0.0.102:6380
3873:S 25 Mar 17:24:33.833 * MASTER <-> SLAVE sync started
3873:S 25 Mar 17:24:33.835 * Non blocking connect for SYNC fired the event.
3873:S 25 Mar 17:24:33.837 * Master replied to PING, replication can continue...
3873:S 25 Mar 17:24:33.840 * Trying a partial resynchronization (request b3a120153f855c5b200783267f6d88655d616318:1).
3873:S 25 Mar 17:24:33.843 * Full resync from master: 6b10906d0f362be8f9dfcb373c47d2ab44f8f805:21930

# master
2654:M 25 Mar 17:24:33.845 * Slave 10.0.0.100:6379 asks for synchronization
2654:M 25 Mar 17:24:33.845 * Partial resynchronization not accepted: Replication ID mismatch (Slave asked for 'b3a120153f855c5b200783267f6d88655d616318', my replication IDs are '6b10906d0f362be8f9dfcb373c47d2ab44f8f805' and 'e5a8131d602c8d58155a74b1bad17fae955431f1')
2654:M 25 Mar 17:24:33.846 * Starting BGSAVE for SYNC with target: disk
2654:M 25 Mar 17:24:33.846 * Background saving started by pid 3089
3089:C 25 Mar 17:24:33.851 * DB saved on disk
3089:C 25 Mar 17:24:33.852 * RDB: 0 MB of memory used by copy-on-write
2654:M 25 Mar 17:24:33.861 * Background saving terminated with success
2654:M 25 Mar 17:24:33.862 * Synchronization with slave 10.0.0.100:6379 succeeded
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容