時(shí)間是河流
如果時(shí)間是一條河流,那么歷史就是無(wú)數(shù)的浪花,佇立在河岸回眸,常常有意識(shí)的想要改變某一朵浪花。

2015年中,一群少年為愛(ài)發(fā)電來(lái)到B站,創(chuàng)建了一個(gè)項(xiàng)目,并寫下了第一行Go代碼。這個(gè)項(xiàng)目后來(lái)成為了B站微服務(wù)、中間件及各類平臺(tái)的孵化器,服務(wù)注冊(cè)與發(fā)現(xiàn)模塊也應(yīng)運(yùn)而生。
時(shí)間回溯到2010年11月,zookeeper正式成為Apache的頂級(jí)項(xiàng)目,標(biāo)志著它是工業(yè)級(jí)的成熟穩(wěn)定產(chǎn)品。再到2015年,etcd剛剛發(fā)布了2.0版本,consul才初出茅廬,我們自然而然的選擇了zookeeper作為我們的服務(wù)注冊(cè)與發(fā)現(xiàn)組件。
魔改net/rpc,我們實(shí)現(xiàn)了性能優(yōu)化 鏈路追蹤trace 鑒權(quán) 超時(shí)傳遞 數(shù)據(jù)采集監(jiān)控 等功能,再加上zookeeper,我們還實(shí)現(xiàn)了net/rpc server服務(wù)注冊(cè) net/rpc client服務(wù)發(fā)現(xiàn) 負(fù)載均衡等功能。
這套框架支撐我們度過(guò)了業(yè)務(wù)快速迭代和頻繁改造的階段,但時(shí)間這條河流奔騰不息,轉(zhuǎn)眼進(jìn)入了2018年。
當(dāng)我們佇立河畔,回首日新月異的新潮技術(shù)變更,凝視一路跌跌撞撞濺起的浪花時(shí),在服務(wù)注冊(cè)與發(fā)現(xiàn)領(lǐng)域,我們已經(jīng)落后了。

落后就要挨打
伴隨B站業(yè)務(wù)的快速發(fā)展,微服務(wù)的節(jié)點(diǎn)數(shù)量幾乎指數(shù)級(jí)增長(zhǎng)。zookeeper逐漸在下面的場(chǎng)景不堪重負(fù):
大量長(zhǎng)連接及session探活,已經(jīng)支撐不了辣么高TPS
CP系統(tǒng),當(dāng)機(jī)房間腦裂后不可用
沒(méi)有專家,出問(wèn)題全靠運(yùn)維三寶:重啟、重裝、換機(jī)器
于是,我們調(diào)研了已經(jīng)成熟的etcd、consul、eureka等流行的服務(wù)注冊(cè)與發(fā)現(xiàn)系統(tǒng),經(jīng)過(guò)一番橫向?qū)Ρ戎螅裱?注冊(cè)中心不能因?yàn)樽陨淼娜魏卧蚱茐姆?wù)之間本身的可連通性,我們選擇了AP系統(tǒng)eureka。
但我們團(tuán)隊(duì)整體都是Go技術(shù)棧,eureka在部署及維護(hù)上讓我們覺(jué)得有些不夠得心應(yīng)手,并且以下幾點(diǎn)在eureka1.0版本中也是已知存在的問(wèn)題:
靠輪詢拉取節(jié)點(diǎn),無(wú)法及時(shí)通知
客戶端拉取全量節(jié)點(diǎn),無(wú)法按需獲取
eureka服務(wù)間數(shù)據(jù)同步隨著業(yè)務(wù)節(jié)點(diǎn)數(shù)增加而成倍增加
沒(méi)有完善的日志支撐節(jié)點(diǎn)變更過(guò)程查詢
沒(méi)有管理面板管理節(jié)點(diǎn)
針對(duì)以上問(wèn)題,eureka官方也推出了2.0版本計(jì)劃,但不幸的是停止開(kāi)發(fā)了(幸虧我們選擇了自研,不然就被坑了
所以,我們決定基于eureka的機(jī)制,打造屬于B站的服務(wù)注冊(cè)與發(fā)現(xiàn)系統(tǒng):Discovery

Discovery
時(shí)間進(jìn)入2018,我們也順應(yīng)技術(shù)的大潮,打造了基于k8s的PAAS平臺(tái),大量的業(yè)務(wù)在準(zhǔn)備和正在遷入。我們制定準(zhǔn)入規(guī)范,將業(yè)務(wù)標(biāo)識(shí)appid、容器啟動(dòng)行為entrypoint、服務(wù)的healthcheck等等進(jìn)行了統(tǒng)一。
最關(guān)鍵的,我們需要統(tǒng)一服務(wù)注冊(cè)!
Discovery在這個(gè)大背景下應(yīng)運(yùn)而生,設(shè)計(jì)之初,我們與運(yùn)維童鞋討論了很多細(xì)節(jié),最終拍定以下設(shè)計(jì)目標(biāo):
實(shí)現(xiàn)AP服務(wù)注冊(cè)與發(fā)現(xiàn)系統(tǒng),保證數(shù)據(jù)最終一致性
與PAAS平臺(tái)結(jié)合,多種發(fā)布方式的自動(dòng)服務(wù)注冊(cè)
網(wǎng)絡(luò)閃斷時(shí)服務(wù)可開(kāi)啟自我保護(hù),保證健康的服務(wù)可用
實(shí)現(xiàn)各個(gè)語(yǔ)言sdk,基于HTTP協(xié)議保證交互簡(jiǎn)易
基本抽象
在Discovery中我們以appid作為服務(wù)的標(biāo)識(shí),以hostname定位實(shí)例instance。定義了三種角色server provider consumer,分別代表:
| 角色 | 功能 |
|---|---|
| server | Discovery服務(wù)節(jié)點(diǎn),提供存儲(chǔ)實(shí)例信息、檢查和剔除無(wú)效節(jié)點(diǎn)、自我保護(hù)等功能 |
| provider | 服務(wù)提供者,提供包括注冊(cè)register、30s周期心跳renew、取消注冊(cè)cancel等功能 |
| consumer | 服務(wù)消費(fèi)者,基于appid獲取所需服務(wù)的節(jié)點(diǎn)信息,并可選30s周期的長(zhǎng)輪詢監(jiān)聽(tīng)服務(wù)及時(shí)變更狀態(tài)通知 |
| instance | 存儲(chǔ)在discovery內(nèi)的實(shí)例信息抽象對(duì)象,包含appid hostname addrs metadata等信息 |
架構(gòu)圖

基本邏輯

provider啟動(dòng)后會(huì)請(qǐng)求Discovery的register接口進(jìn)行實(shí)例信息注冊(cè),注冊(cè)成功后要進(jìn)行30s周期一次的renew心跳,用于維護(hù)Discovery內(nèi)在線狀態(tài)。

consumer啟動(dòng)后請(qǐng)求Discovery的fetch接口,根據(jù)appid獲取所有的實(shí)例信息。如果有實(shí)時(shí)接收appid變更的需要,可以請(qǐng)求poll接口進(jìn)行長(zhǎng)輪詢,首次請(qǐng)求會(huì)拿到server節(jié)點(diǎn)下發(fā)的latestTimestamp(表示appid的最后變更時(shí)間,該時(shí)間為server自身時(shí)間且不server間同步)。當(dāng)再有變更發(fā)生時(shí)Discovery更新自身latestTimestamp,與consumer請(qǐng)求時(shí)攜帶的latestTimestamp對(duì)比,如超過(guò)則下發(fā)最新實(shí)例信息,否則維持長(zhǎng)輪詢連接直到30s超時(shí)或有變更發(fā)生。

server開(kāi)始會(huì)收到appid的某一個(gè)實(shí)例的注冊(cè)請(qǐng)求,在內(nèi)存中存儲(chǔ)為instance,通過(guò)Peer to Peer將數(shù)據(jù)同步給其他server節(jié)點(diǎn),之后實(shí)例會(huì)進(jìn)行每30s一次的renew心跳請(qǐng)求,并經(jīng)過(guò)LB后打給任意一個(gè)server節(jié)點(diǎn),節(jié)點(diǎn)間再通過(guò)P2P進(jìn)行數(shù)據(jù)同步,每次renew都會(huì)更新server內(nèi)instance的renewTimestamp時(shí)間戳。當(dāng)該實(shí)例發(fā)送cancel取消注冊(cè)請(qǐng)求后,server節(jié)點(diǎn)將從內(nèi)存中將該instance信息刪除。
server運(yùn)行期間則會(huì)進(jìn)行每90s一個(gè)周期的心跳請(qǐng)求檢測(cè),當(dāng)90s周期內(nèi)某一instance最近一次的renewTimestamp比當(dāng)前時(shí)間小于90s,則判斷該instance失效并刪除。為了避免網(wǎng)絡(luò)故障而導(dǎo)致90s內(nèi)大量instance全無(wú)心跳被全部刪除的情況,server內(nèi)還會(huì)進(jìn)行每60s周期一次的自我保護(hù)判斷,當(dāng) (60s內(nèi)收集的所有心跳數(shù)) 小于 (所有instance的總數(shù)*2*0.85) 時(shí)進(jìn)入自我保護(hù)模式,此時(shí)每90s的刪除檢測(cè)會(huì)無(wú)效,否則取消自我保護(hù),恢復(fù)正常模式。而為了避免確實(shí)有大量節(jié)點(diǎn)突然掛掉(或其他異常情況)而觸發(fā)進(jìn)入自我保護(hù)模式但無(wú)法恢復(fù)為正常模式的情況,設(shè)置了最大自我保護(hù)時(shí)間2h,當(dāng)超過(guò)2h還處于自我保護(hù)模式,則自動(dòng)恢復(fù)為正常模式。
重要邏輯
- 復(fù)制(Peer to Peer),數(shù)據(jù)一致性的保障:
-
appid注冊(cè)時(shí)根據(jù)當(dāng)前時(shí)間生成dirtyTimestamp,Discovery的serverA向serverB同步(register)時(shí),serverB可能有以下兩種情況:- 返回
-404則serverA攜帶dirtyTimestamp向serverB發(fā)起注冊(cè)請(qǐng)求,把最新信息同步:-
serverB中不存在實(shí)例 -
serverB中dirtyTimestamp較小
-
- 返回
-409serverB不同意采納serverA信息,且返回自身信息,serverA使用該信息更新自身
- 返回
-
appid注冊(cè)成功后,provider每30s發(fā)起一次renew請(qǐng)求,處理流程同上
-
-
instance管理- 正常檢測(cè)模式,隨機(jī)分批踢掉無(wú)心跳
instance節(jié)點(diǎn),盡量避免單應(yīng)用節(jié)點(diǎn)被一次全踢 - 網(wǎng)絡(luò)閃斷和分區(qū)時(shí)自我保護(hù)模式
- 60s內(nèi)丟失大量(小于
instance總數(shù)*2*0.85)心跳數(shù),“好”“壞”instance信息都保留 - 所有
server都會(huì)持續(xù)提供服務(wù),單個(gè)server的注冊(cè)和發(fā)現(xiàn)功能不受影響 - 最大保護(hù)時(shí)間,防止分區(qū)恢復(fù)后大量原先
instance真的已經(jīng)不存在時(shí),一直處于保護(hù)模式
- 60s內(nèi)丟失大量(小于
- 正常檢測(cè)模式,隨機(jī)分批踢掉無(wú)心跳
-
consumer客戶端- 長(zhǎng)輪詢+
server推送,服務(wù)發(fā)現(xiàn)準(zhǔn)實(shí)時(shí) - 訂閱式,只需要關(guān)注想要關(guān)注的
appid的instance列表變化 - 緩存實(shí)例
instance列表信息,保證與server網(wǎng)絡(luò)不通等無(wú)法訪問(wèn)到server情況時(shí)原先的instance可用
- 長(zhǎng)輪詢+
特別注意
server間同步復(fù)制是需要時(shí)間的,那如何保證consumer請(qǐng)求serverB時(shí),因?yàn)閿y帶的latestTimestamp是來(lái)自serverA,但serverB晚于該次請(qǐng)求才收到同步事件,而導(dǎo)致獲取的節(jié)點(diǎn)信息不一致?
我們通過(guò)consumer啟動(dòng)后,從nodes接口獲取到Discovery的所有server節(jié)點(diǎn)后,隨機(jī)選取一個(gè)serverA進(jìn)行fetch poll等請(qǐng)求,保證在consumer生命周期內(nèi),實(shí)例信息和時(shí)間信息始終來(lái)自同一個(gè)serverA。除非遇到網(wǎng)絡(luò)等錯(cuò)誤才切換節(jié)點(diǎn)到serverB并清空latestTimestamp,再當(dāng)做首次請(qǐng)求重新拉取appid的全部實(shí)例信息和時(shí)間信息。
多注冊(cè)中心
Discovery的同步復(fù)制機(jī)制天生好支持多注冊(cè)中心。

我們用zone來(lái)表示機(jī)房,假設(shè)zoneA和zoneB的Discovery集群之間要相互同步,那我們只需要將zoneA當(dāng)做zoneB的特殊server節(jié)點(diǎn),同理將zoneB當(dāng)做zoneA的特殊server節(jié)點(diǎn)。
當(dāng)zoneA的serverA收到appid1的注冊(cè)請(qǐng)求,并同步給內(nèi)部的其他server后,再同步給server-zoneB,zoneB即可復(fù)制到appid1的實(shí)例信息。
但zoneB內(nèi)部server間同步后不再需要同步回zoneA,所以特殊server就是指在發(fā)送同步請(qǐng)求時(shí),判斷該請(qǐng)求是否來(lái)自相同的zone,是的話就像zoneA同步給zoneB,否的話就像zoneB內(nèi)部同步后不再向其他zone同步。
注:zoneA與zoneB間,建議使用SLB進(jìn)行負(fù)載均衡
與PAAS在一起

我們的PAAS平臺(tái)已經(jīng)集成了Discovery的服務(wù)注冊(cè),也就是provider能力。業(yè)務(wù)只需要正常發(fā)布就可以直接注冊(cè)到Discovery,并依賴pod的生命周期進(jìn)行renew心跳請(qǐng)求管理。
如果服務(wù)需要提供RPC、集群、權(quán)重等自定義信息,則只需要暴露 /register 接口并返回map[string]string格式的json數(shù)據(jù),PAAS在啟動(dòng)實(shí)例后和注冊(cè)信息前,通過(guò)回調(diào)該接口獲取信息,將信息作為metedata同時(shí)注冊(cè)到Discovery。
基于此,依賴服務(wù)(consumer)就可以獲取到實(shí)例信息,并對(duì)服務(wù)進(jìn)行訪問(wèn)。
管理節(jié)點(diǎn)

我們還實(shí)現(xiàn)了簡(jiǎn)單的管理能力,可以基于appid和環(huán)境信息獲取到所有實(shí)例信息。并基于此擴(kuò)展了查詢依賴服務(wù) 生成CPU和內(nèi)存profile圖 火焰圖等功能。
奔騰不息的河流
當(dāng)我們?cè)僖淮蝸辛⒃诤影痘仨?,發(fā)現(xiàn)時(shí)光的浪花翻騰,但總有那么幾朵浪花丑陋,讓人想要在今后扔石子時(shí),扔的漂亮~
結(jié)語(yǔ)
Discovery已經(jīng)服務(wù)于B站幾萬(wàn)+的實(shí)例規(guī)模,通過(guò)借此總結(jié)我們?cè)诜?wù)注冊(cè)與發(fā)現(xiàn)領(lǐng)域的實(shí)踐經(jīng)驗(yàn),希望對(duì)業(yè)界閱讀此文的童鞋能夠有所幫助和啟發(fā)。同時(shí),我們也希望收到大家的反饋意見(jiàn),詳情請(qǐng)看Discovery開(kāi)源項(xiàng)目【點(diǎn)我到Github】。
本文作者:冠冠愛(ài)看書