Etcd原始碼分析-網路模型
從本篇開始,將會有一個系列對Etcd原始碼進行分析。
我之前閱讀過很多開源軟體,對閱讀開源軟體,有如下基本思路:
1、瞭解該軟體相關背景知識,例如相關部落格、官網,要相信自己不是第一個分析該軟體的人
2、對該軟體進行使用,例如:編譯、執行或者基於介面進行開發
3、找到該軟體的合適切入點進行原始碼分析,例如網路相關的軟體(ovs、etcd)找到socket監聽服務、接收訊息、傳送訊息
對於etcd這種分散式儲存系統,我們的切入點就是socket服務。
一、配置檔案
1.1、配置檔案簡介
研究一個軟體最好的方式就是先把配置檔案搞清楚,這個是根基。我們看一下etcd的配置檔案,預設儲存位置為/etc/etcd/etcd.conf,該配置檔案一共有5個section分別是:
名稱 | 作用 |
member | 本節點的配置,包括監聽服務埠、心跳時間等 |
cluster | 叢集配置,包括叢集狀態、叢集名稱以及本節點廣播地址 |
proxy | 用於網路自動發現服務 |
security | 安全配置 |
logging | 日誌功能元件 |
其中配置檔案中比較重要的是member和cluster配置項。
1.2、部分引數詳細說明
1.2.1、member
在Etcd中一共監聽了兩個服務埠分別2379和2380,對應在member中體現的如下兩個配置項:
名稱 | 舉例 | 作用 |
ETCD_LISTEN_CLIENT_URLS | ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379" | 2379埠用於客戶端訪問etcd資料庫,例如向etcd中插入資料。 |
ETCD_LISTEN_PEER_URLS | ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380" | 2380埠用於叢集成員間進行通訊,例如叢集資料同步、心跳。 |
1.2.2、cluster
初次看到配置檔案,都會有一個疑問,為什麼在members已經設定了監聽服務地址,為什麼在cluster還要再次設定一次廣播地址呢?原因:etcd主要的通訊協議主要是http協議,對於http協議中所周知它是B/S結構,而非C/S結構,只能一端主動給另一端發訊息而反過來則不可。所以對於叢集來說,雙方必須都要知道對方具體監聽地址。
名稱 | 舉例 | 作用 |
ETCD_ADVERTISE_CLIENT_URLS | ETCD_LISTEN_CLIENT_URLS="http://10.10.10.128:2379" | 同上表 |
ETCD_INITIAL_ADVERTISE_PEER_URLS | ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.10.10.128:2380" | 同上表 |
叢集相關配置,在後面介紹Raft叢集時再進行詳細說明。
二、服務監聽
從本小節開始介紹詳細程式碼。我們都知道,建立socket服務端一共有5個基本步驟(C語言):建立socket套接字、bind地址及埠、listen監聽服務、accept接收客戶端連線、啟動新執行緒為客戶端服務。正所謂萬變不離其宗,到了etcd中(etcd使用預設golang http模組)也是這些步驟,只不過是被封裝了一下(語法糖)。
2.1、總體流程圖
從這裡開始介紹具體流程以及關鍵程式碼,對於資料結構會有專門一篇介紹,如下是從main方法入口函式:
流程圖簡要說明:
1、方法startEtcdOrProxyV2()中,會根據配置檔案啟動兩種不同模式:預設模式和代理模式。預設模式進入方法startEtcd,代理模式進入方法startProxy。這裡介紹預設模式。
2、方法newConfig,從名字上來看就知道,此方法是用於讀取配置檔案或者生成預設配置,在1.2章節中介紹的配置項就是在此方法中讀取。
3、執行完方法serve後,會退回到startEtcd中,然後就阻塞在startEtcd方法中,這樣整個etcd啟動完畢。
2.2、核心方法embed/etcd.go StartEtcd
此方法內容比較長,分為三部分說明: /*
* 構建Etcd結構 包含一個server、listener
*/
serving := false
e = &Etcd{cfg: *inCfg, stopc: make(chan struct{})} //etcd結構體
cfg := &e.cfg
defer func() {//解構函式
if e == nil || err == nil {
return
}
if !serving {
// errored before starting gRPC server for serveCtx.grpcServerC
for _, sctx := range e.sctxs {
close(sctx.grpcServerC)
}
}
e.Close()
e = nil
}()
if e.Peers, err = startPeerListeners(cfg); err != nil {//為peer建立listener,socket三部曲只到了第二個步驟
return
}
if e.sctxs, err = startClientListeners(cfg); err != nil {//為client建立listener,socket三部曲只到了第二個步驟
return
}
for _, sctx := range e.sctxs {
e.Clients = append(e.Clients, sctx.l)
}
上面完成etcd結構初始化以及listener建立。
// 建立EtcdServer
srvcfg := &etcdserver.ServerConfig{
Name: cfg.Name,
ClientURLs: cfg.ACUrls, //客戶端url監聽地址,2379埠
PeerURLs: cfg.APUrls, //peer url監聽地址,2380埠
DataDir: cfg.Dir,
DedicatedWALDir: cfg.WalDir,
SnapCount: cfg.SnapCount,
MaxSnapFiles: cfg.MaxSnapFiles,
MaxWALFiles: cfg.MaxWalFiles,
InitialPeerURLsMap: urlsmap,
InitialClusterToken: token,
DiscoveryURL: cfg.Durl,
DiscoveryProxy: cfg.Dproxy,
NewCluster: cfg.IsNewCluster(), /* 是否新的叢集 */
ForceNewCluster: cfg.ForceNewCluster,
PeerTLSInfo: cfg.PeerTLSInfo,
TickMs: cfg.TickMs,
ElectionTicks: cfg.ElectionTicks(),
AutoCompactionRetention: cfg.AutoCompactionRetention,
QuotaBackendBytes: cfg.QuotaBackendBytes,
StrictReconfigCheck: cfg.StrictReconfigCheck,
ClientCertAuthEnabled: cfg.ClientTLSInfo.ClientCertAuth,
AuthToken: cfg.AuthToken,
}
//建立EtcdServer並且建立raftNode並執行raftNode
if e.Server, err = etcdserver.NewServer(srvcfg); err != nil {
return
}
// configure peer handlers after rafthttp.Transport started
// 生成http.hander 用於處理peer請求
ph := etcdhttp.NewPeerHandler(e.Server)
for _, p := range e.Peers {
srv := &http.Server{
Handler: ph,
ReadTimeout: 5 * time.Minute,
ErrorLog: defaultLog.New(ioutil.Discard, "", 0), // do not log user error
}
l := p.Listener //上一段程式碼建立的listener
p.serve = func() error { return srv.Serve(l) } //回撥函式,啟用服務,主要是Accept方法
p.close = func(ctx context.Context) error {關閉服務,回撥掉函式。即socket關閉時呼叫此方法
// gracefully shutdown http.Server
// close open listeners, idle connections
// until context cancel or time-out
return srv.Shutdown(ctx)
}
}
上面handler用於處理peer發過來的請求以及設定回撥函式。
// buffer channel so goroutines on closed connections won't wait forever
e.errc = make(chan error, len(e.Peers)+len(e.Clients)+2*len(e.sctxs))
//執行EtcdSever 監聽服務
e.Server.Start()
if err = e.serve(); err != nil {//啟用服務,主要呼叫第二段程式碼的回撥函式serve
return
}
serving = true
下面重點分析一下listerner以及serve回撥函式。
2.3、Listener分析
Listener有兩個分別為:peer listener和client listener,兩者大同小異,這裡拿peer listener做為分析物件。
方法startPeerListeners,中主要核心程式碼,如下:
for i, u := range cfg.LPUrls {//迴圈遍歷多個peer url
if u.Scheme == "http" {
if !cfg.PeerTLSInfo.Empty() {
plog.Warningf("The scheme of peer url %s is HTTP while peer key/cert files are presented. Ignored peer key/cert files.", u.String())
}
if cfg.PeerTLSInfo.ClientCertAuth {
plog.Warningf("The scheme of peer url %s is HTTP while client cert auth (--peer-client-cert-auth) is enabled. Ignored client cert auth for this url.", u.String())
}
}
/* 構造peerListener物件 監聽2380 作為服務端模式 */
peers[i] = &peerListener{close: func(context.Context) error { return nil }}
//呼叫介面,建立listener物件,返回來之後,
//socket套接字已經完成listener監聽流程
peers[i].Listener, err = rafthttp.NewListener(u, &cfg.PeerTLSInfo)
if err != nil {
return nil, err
}
// once serve, overwrite with 'http.Server.Shutdown'
// close回撥方法,用於關閉socket套接字
peers[i].close = func(context.Context) error {
return peers[i].Listener.Close()
}
plog.Info("listening for peers on ", u.String())
}
func newListener(addr string, scheme string) (net.Listener, error) {
if scheme == "unix" || scheme == "unixs" {
// unix sockets via unix://laddr
return NewUnixListener(addr)
}
return net.Listen("tcp", addr) //呼叫golang內建方法,返回listener物件
}
從startPeerListeners到net.Listen整個流程中,有摻雜tls以及unix socket相關邏輯,新增這些只為了保證各種需求,大體流程並沒有變化,這裡不在對齊進行詳細說明。至此,兩個服務均已完成監聽步驟,下面就是接收對端請求即Accept過程。
三、服務啟用
在上面已經介紹了,服務端socket需要呼叫Accept方法,我們來看一下serve方法。方法serve大致內容為:將每個服務放到gorouting中,也就是啟動一個協程來監聽服務。func (e *Etcd) serve() (err error) {
var ctlscfg *tls.Config
if !e.cfg.ClientTLSInfo.Empty() {
plog.Infof("ClientTLS: %s", e.cfg.ClientTLSInfo)
if ctlscfg, err = e.cfg.ClientTLSInfo.ServerConfig(); err != nil {
return err
}
}
if e.cfg.CorsInfo.String() != "" {
plog.Infof("cors = %s", e.cfg.CorsInfo)
}
// Start the peer server in a goroutine
// 為Peer啟動協程
for _, pl := range e.Peers {
go func(l *peerListener) {
// 叢集peer 前期已經建立listener,此處將會呼叫accept,
// 那麼serve()是什麼地方定義的?
e.errHandler(l.serve())
}(pl)
}
// Start a client server goroutine for each listen address
// 為Client啟動協程
var h http.Handler
if e.Config().EnableV2 {
h = v2http.NewClientHandler(e.Server, e.Server.Cfg.ReqTimeout())
} else {
mux := http.NewServeMux()
etcdhttp.HandleBasic(mux, e.Server)
h = mux
}
h = http.Handler(&cors.CORSHandler{Handler: h, Info: e.cfg.CorsInfo})
for _, sctx := range e.sctxs {
go func(s *serveCtx) {
// client 前期已經建立listener,此處將呼叫accept
e.errHandler(s.serve(e.Server, ctlscfg, h, e.errHandler))
}(sctx)
}
return nil
}
那麼在l.serve()或者s.serve()在什麼地方定義的?在方法StartEtcd中生成http.PeerHandler附近,在第二章節已經有介紹。我們來看一下具體內容:srv := &http.Server{
Handler: ph, //http handler 用於處理業務邏輯 後面呼叫handler中ServeHTTP方法
ReadTimeout: 5 * time.Minute,
ErrorLog: defaultLog.New(ioutil.Discard, "", 0), // do not log user error
}
l := p.Listener
p.serve = func() error { return srv.Serve(l) } //啟用服務,這地方呼叫的srv.Serve(l),此方法呼叫golang內建方法http.Server.
下面簡要分析一下http.Server方法:
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
if fn := testHookServerServe; fn != nil {
fn(srv, l)
}
var tempDelay time.Duration // how long to sleep on accept failure
if err := srv.setupHTTP2_Serve(); err != nil {
return err
}
srv.trackListener(l, true)
defer srv.trackListener(l, false)
baseCtx := context.Background() // base is always background, per Issue 16220
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {//死迴圈
rw, e := l.Accept() //阻塞等待客戶端程式連線
....
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx) //表示啟動協程,服務新的連線。
}
}
此處的Accept定義在什麼地方呢?Accept為golang內建方法,定義在Go/src/net/tcpsock.go,對於Accept內容不再展開,只要知道它會一直阻塞,直到有新的連線才會返回。
這裡著重介紹一下http/server.go中serve方法。在serve方法內部主要內容是一個for迴圈:
for {
w, err := c.readRequest(ctx) //接收到對端傳送的http請求
if c.r.remain != c.server.initialReadLimitSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive)
}
...
// Expect 100 Continue support
req := w.req
if req.expectsContinue() {
if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
// Wrap the Body reader with one that replies on the connection
req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
}
} else if req.Header.get("Expect") != "" {
w.sendExpectationFailed()
return
}
c.curReq.Store(w)
if requestBodyRemains(req.Body) {
registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
} else {
if w.conn.bufr.Buffered() > 0 {
w.conn.r.closeNotifyFromPipelinedRequest()
}
w.conn.r.startBackgroundRead()
}
// HTTP cannot have multiple simultaneous active requests.[*]
// Until the server replies to this request, it can't read another,
// so we might as well run the handler in this goroutine.
// [*] Not strictly true: HTTP pipelining. We could let them all process
// in parallel even if their responses need to be serialized.
// But we're not going to implement HTTP pipelining because it
// was never deployed in the wild and the answer is HTTP/2.
serverHandler{c.server}.ServeHTTP(w, w.req) //處理http請求
w.cancelCtx()
if c.hijacked() {
return
}
w.finishRequest() //將緩衝區資料傳送到對端,完成http此次請求
...
}
呼叫c.readRequest(ctx)方法等待對端發來的請求,呼叫serverHandler{c.server}.ServeHTTP(w,w.req)處理客戶端請求並且傳送到對端。來看一下ServeHTTP實現:
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*"&& req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
會發現在內部會呼叫handler.ServeHTTP方法,那麼此處的handler在什麼地方定義的?ServeHTTP又在哪裡定義的?繼續看下面章節
四、接收流程
上一章節介紹呼叫handler.ServeHTTP,可想而知,這個是golang內建的http框架,框架不知道具體業務需求是什麼,所以一般場景下handler都是使用者自定義的,使用者根據不同業務需求來實現不同的handler。對Etcd中存在很多handler,後面會文章進行詳細分析。
現在解答一下,handler在什麼地方定義以及ServeHTTP定義在什麼地方?這個其實在第三章就已經有提到,在建立完listener會建立一個handler,如etcdhttp.NewPeerHandler(e.Server),處理叢集節點內訊息。
先來看一下建立的handler裡面有什麼內容?用peerhandler作為例子說明,在構造方法NewPeerHandler中主要呼叫newPeerHandler方法:func newPeerHandler(cluster api.Cluster, raftHandler http.Handler, leaseHandler http.Handler) http.Handler {
mh := &peerMembersHandler{
cluster: cluster,
}
//將url和業務層handler註冊到servemux中,也就是每一個url請求都會有其對應的handler進行處理。
mux := http.NewServeMux() //初始化一個Serve Multiplexer結構
mux.HandleFunc("/", http.NotFound)
mux.Handle(rafthttp.RaftPrefix, raftHandler) /* rafthttp.RaftPrefix == /raft */
mux.Handle(rafthttp.RaftPrefix+"/", raftHandler)
mux.Handle(peerMembersPrefix, mh) //處理請求/members handler是mh,即peerMembersHandler
if leaseHandler != nil {
mux.Handle(leasehttp.LeasePrefix, leaseHandler) /* /leases */
mux.Handle(leasehttp.LeaseInternalPrefix, leaseHandler) /* /leases/internal */
}
mux.HandleFunc(versionPath, versionHandler(cluster, serveVersion))
return mux
}
通過上面的程式碼可知,應用層業務邏輯需要自己註冊url和handler,這樣才能保證每個http request都能夠被處理。而每個handler都必須要實現對應介面ServeHTTP,例如peerMembersHandler,實現的ServeHTTP介面是用於返回叢集成員列表。那麼此處只是完成註冊,那麼在什麼地方會呼叫此處handler?接下來看一下golang內建方法。
流程圖中所有方法均在Go/src/net/http/server.go中。通過流程圖可知:
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r) //登錄檔查詢,使用者自定義的handler,並且呼叫ServeHTTP介面,處理該http request請求。
h.ServeHTTP(w, r)
}
五、傳送流程
對於傳送流程,這裡打算簡單介紹一下,後續在介紹raft協議是會詳細說明。
通過之前介紹,ServeHTTP介面是用於處理http request請求入口,每一個handler都必須實現此介面。此介面有兩個引數,分別為:ResponseWriter,Request。其中ResponseWriter就是用於響應,http請求,這裡用peerMembersHandler作為舉例(比較簡單),程式碼如下:
/* 獲取叢集成員列表 */
func (h *peerMembersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r, "GET") {
return
}
w.Header().Set("X-Etcd-Cluster-ID", h.cluster.ID().String())
if r.URL.Path != peerMembersPrefix {
http.Error(w, "bad path", http.StatusBadRequest)
return
}
ms := h.cluster.Members() //獲取叢集成員列表
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(ms); err != nil {//呼叫json介面,進行格式化並且將資料寫到緩衝區中
plog.Warningf("failed to encode members response (%v)", err)
}
}
通過檢視Encode程式碼可知,最後會將會資料寫入到ResponseWriter的緩衝區中,即呼叫Wirter介面,並沒有真正傳送到對端。那麼在什麼地方才會真正發出去呢?在golang http原始碼w.finishRequest(),此處會進行重新整理操作,將緩衝區資料傳送到對端。在上面已經提交到。六、總結
至此,介紹Etcd網路通訊流程就結束了,下一篇介紹網路模型進階篇。
相關推薦
Etcd原始碼分析-網路模型
從本篇開始,將會有一個系列對Etcd原始碼進行分析。我之前閱讀過很多開源軟體,對閱讀開源軟體,有如下基本思路:1、瞭解該軟體相關背景知識,例如相關部落格、官網,要相信自己不是第一個分析該軟體的人2、對該軟體進行使用,例如:編譯、執行或者基於介面進行開發3、找到該軟體的合適切入
Etcd原始碼分析-網路模型進階篇
起初本篇打算介紹raft相關,但是後來發現,還是有必要再深入介紹一下網路模型。一、基礎網路模型 Etcd採用http(https)協議作為應用層協議,關於http協議介紹不是本篇範疇。大家都知道http一般情況下是無狀態協議,且網路是位請求+應答,當收到應答ht
Memcached原始碼分析-網路模型(1)
1 網路模型 Memcached採用了,單程序多執行緒的工作方式,同時採用了libevent事件驅動進行網路請求的處理。 2 工作原理 2.1 libevent介紹 2.2 網路請求流程 2.2.1 流程圖 2.2.2 主執行緒工作流程分析 主執行緒工作流
比特幣原始碼分析-網路(二)
比特幣原始碼分析-網路(二) https://www.jianshu.com/p/4b42d8698f35 眾所周知,比特幣網路是採用的P2P網路體系,所以,沒有明顯的客戶端與服務端的區別或者是概念,每一個節點既是自身的客戶端,又是其它節點的服務端。 在sync.h中,
Netty原始碼分析--Reactor模型(二)
這一節和我一起開始正式的去研究Netty原始碼。在研究之前,我想先介紹一下Reactor模型。 我先分享兩篇文獻,大家可以自行下載學習。 連結:https://pan.baidu.com/s/1Utym7
Netty原始碼分析--記憶體模型(上)(十一)
前兩節我們分別看了FastThreadLocal和ThreadLocal的原始碼分析,並且在第八節的時候講到了處理一個客戶端的接入請求,一個客戶端是接入進來的,是怎麼註冊到多路複用器上的。那麼這一節我們來一起看下客戶端接入完成之後,是怎麼實現讀寫操作的?我
Netty原始碼分析--記憶體模型(下)(十二)
這一節我們一起看下分配過程 1 PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) { 2 PooledByteBuf
HotSpot原始碼分析之類模型
HotSpot採用了OOP-Klass模型描述Java的類和物件。Klass模型採用Klass類及相關子類的物件來描述具體的Java類。一般HotSpot JVM 在載入Java的Class 檔案時,會在方法區建立 Klass ,用來儲存Java類的元資料,包括常量池、欄位、方法等。 Klass模型中相關類的
Memcached原始碼分析之基於Libevent的網路模型(1)
文章列表: 《Memcached原始碼分析 - Memcached原始碼分析之總結篇(8)》 關於Memcached: memcached是一款非常普及的伺服器端快取軟體,memcached主要是基於Libevent庫進行開發的。 如果你還不瞭解libev
Lighttpd1.4.20原始碼分析 筆記 網路服務主模型
一.概述 Lighttpd採用多程序網路服務模型。 程序分兩種:監控程序watcher 和 工作程序 workers。 監控程序:fork工作程序並監視工作程序的數目,一旦有工作程序退出,監控程序立即fork新的工作程序。 工作程序:接收客戶端請求並做出
Redis網路模型的原始碼分析
Redis的網路模型是基於I/O多路複用程式來實現的。原始碼中包含四種多路複用函式庫epoll、select、evport、kqueue。在程式編譯時會根據系統自動選擇這四種庫其中之一。下面以epoll為例,來分析Redis的I/O模組的原始碼。 ## epoll系統呼叫方法 Redis網路事件處理模組的程式
PyTorch--雙向遞迴神經網路(B-RNN)概念,原始碼分析
關於概念: BRNN連線兩個相反的隱藏層到同一個輸出.基於生成性深度學習,輸出層能夠同時的從前向和後向接收資訊.該架構是1997年被Schuster和Paliwal提出的.引入BRNNS是為了增加網路所用的輸入資訊量.例如,多層感知機(MLPS)和延時神經網路(TDNNS)在輸入資料的靈活性方面是非
【kubernetes/k8s原始碼分析】kubelet原始碼分析之容器網路初始化原始碼分析
一. 網路基礎 1.1 網路名稱空間的操作 建立網路名稱空間: ip netns add 名稱空間內執行命令: ip netns exec 進入名稱空間: ip netns exec bash 1.2 bridge-nf-c
【ArcGIS|空間分析|網路分析】6 建立路徑分析模型
參考ArcGIS幫助文件 文章目錄 要求 步驟 1 在模型中建立路徑圖層 2 將停靠點新增至路徑分析圖層 3 新增“求解”工具 4 執行模型以查詢最佳路徑 5 配置模型以將結果儲存到磁碟
k8s與網路--Flannel原始碼分析
前言 之前在k8s與網路--Flannel解讀一文中,我們主要講了Flannel整體的工作原理。今天主要針對Flannel v0.10.0版本進行原始碼分析。首先需要理解三個比較重要的概念: 網路(Network):整個叢集中分配給 flannel 要管理的網路地
以太坊p2p網路(五):P2P模組TCP連線池網路通訊機制原始碼分析
上節中通過設定靜態節點BootstrapNodes節點來發現更多全網的其他節點,這部分只是發現節點並找出其中可以ping通的節點,但是還沒有進行使用,還沒建立TCP連線進行資料傳輸,協議處理等。 這裡主要分析P2P系統的TCP連線池的建立,以及是怎麼跟其他節點通
Netflix Eureka原始碼分析(18)——eureka server網路故障時的的自我保護機制原始碼剖析
假如說,20個服務例項,結果在1分鐘之內,只有8個服務例項保持了心跳 --> eureka server是應該將剩餘的12個沒有心跳的服務例項都摘除嗎? 這個時候很可能說的是,eureka server自己網路故障了,那些服務沒問題的。只不過eureka server
ETCD v2 原始碼分析
Etcd is a distributed, consistent key-valuestore for shared configurationand service discovery ETCD 有 v2和 v3,api 和內部儲存都不一樣。這裡只分析 v
OpenCV兩種畸變校正模型原始碼分析以及CUDA實現
http://www.cnblogs.com/riddick/p/7811877.html 影象演算法中會經常用到攝像機的畸變校正,有必要總結分析OpenCV中畸變校正方法,其中包括普通針孔相機模型和魚眼相機模型fisheye兩種畸變校正方法。 普通相機模型畸變校正函式針對OpenCV中的c
比特幣原始碼分析--P2P網路初始化
區塊鏈和AI無疑是近期業界當之無愧的兩大風口。AI就不說了,區塊鏈從17年各種數字貨幣被炒上了天,一下成為了人們街頭巷議的焦點,本文撇開數字貨幣的投資不說,僅僅從技術層面來剖析一下區塊鏈各個部分的原理。畢竟目前已經有包括BAT等巨頭在內的許多公司投入到了區塊鏈的研發