Lyft微服務(wù)研發(fā)效能提升實(shí)踐 | 3. 利用覆蓋機(jī)制在預(yù)發(fā)環(huán)境中擴(kuò)展服務(wù)網(wǎng)格

怎樣才能提高研發(fā)效率?是依賴于各自獨(dú)立的本地開發(fā)測試環(huán)境,還是依賴完整的端到端測試?Lyft的這一系列文章介紹了其開發(fā)環(huán)境的歷史和發(fā)展,幫助我們思考如何打造一套適合大規(guī)模微服務(wù)的高效研發(fā)環(huán)境。本系列共4篇文章,這是第3篇。原文:Scaling productivity on microservices at Lyft (Part 3): Extending our Envoy mesh with staging overrides[1]

本系列介紹的是Lyft在面對越來越多的開發(fā)人員和服務(wù)時(shí),如何高效擴(kuò)展開發(fā)實(shí)踐,本文是第三篇。

在之前的文章中,我們描述了為快速迭代本地服務(wù)而設(shè)計(jì)的筆記本電腦開發(fā)工作流程。在這篇文章中,將詳細(xì)介紹安全且隔離的端到端(E2E)測試解決方案:預(yù)生產(chǎn)共享環(huán)境。在深入研究實(shí)現(xiàn)細(xì)節(jié)之前,我們將簡要回顧促使我們構(gòu)建這一系統(tǒng)的問題。

以前的集成環(huán)境

在本系列的第1部分中,我們介紹了在之前用于多服務(wù)端到端測試的工具Onebox。Onebox的用戶需要租用大型AWS EC2虛擬機(jī)來啟動100多個(gè)服務(wù),以驗(yàn)證修改是否能夠跨服務(wù)邊界工作。這種解決方案為每個(gè)開發(fā)人員提供了一個(gè)沙盒,運(yùn)行自己版本的Lyft,控制每個(gè)服務(wù)的版本、數(shù)據(jù)庫內(nèi)容和運(yùn)行時(shí)配置。

每個(gè)開發(fā)人員都運(yùn)行并管理自己獨(dú)立的Onebox

不幸的是,隨著Lyft的工程師和服務(wù)數(shù)量的增加,Onebox遇到了規(guī)模問題(詳情見第一篇文章),我們需要找到可持續(xù)的替代方案來執(zhí)行端到端測試。

我們將共享的預(yù)發(fā)環(huán)境視為一種可行的替代品。預(yù)發(fā)環(huán)境與生產(chǎn)環(huán)境的相似性讓我們充滿信心,但我們需要添加缺失的部分,從而提供一個(gè)安全的開發(fā)環(huán)境:隔離。

預(yù)發(fā)環(huán)境(Staging Environment)

預(yù)發(fā)環(huán)境運(yùn)行與生產(chǎn)環(huán)境相同的技術(shù)棧,但使用了彈性資源、模擬用戶數(shù)據(jù)以及人造web流量生成器。預(yù)發(fā)環(huán)境是Lyft一級環(huán)境,如果環(huán)境變得不穩(wěn)定,SLO[2]受到影響,隨時(shí)待命的工程師和開發(fā)人員就會提升SEV[3]。盡管預(yù)發(fā)環(huán)境的可用性和真正的流量增加了端到端的可信度,但如果我們鼓勵(lì)廣泛使用預(yù)發(fā)環(huán)境,就可能會出現(xiàn)一些問題:

  1. 預(yù)發(fā)環(huán)境是完全共享的環(huán)境,就像生產(chǎn)環(huán)境一樣,如果有人將一個(gè)故障實(shí)例部署到預(yù)發(fā)集群,就會影響到其他(可能是傳遞性的)依賴該服務(wù)的人。

  2. 交付新代碼的方式是將PR合并到主線,從而觸發(fā)一個(gè)新的部署流水線。為了測試實(shí)驗(yàn)性變更如何在端到端環(huán)境中工作,需要承受大量的過程負(fù)擔(dān):編寫測試、代碼審查、合并,并通過CI/CD進(jìn)行進(jìn)展。

  3. 這個(gè)繁重的過程可能會導(dǎo)致用戶使用“逃生口”:將PR分支直接部署到預(yù)發(fā)環(huán)境。當(dāng)未處理的提交在預(yù)發(fā)環(huán)境下運(yùn)行時(shí),將進(jìn)一步放大降低環(huán)境穩(wěn)定性的缺陷問題。

我們的目標(biāo)是克服這些挑戰(zhàn),使預(yù)發(fā)環(huán)境更適合手工驗(yàn)證端到端工作流。我們想讓用戶在準(zhǔn)備階段測試他們的代碼,而不是被過程所困。如果他們的修訂出現(xiàn)問題的話,最小化變更的影響半徑。為了實(shí)現(xiàn)這一點(diǎn),我們創(chuàng)建了staging override。

Staging Overrides(預(yù)發(fā)覆蓋)

Staging override是一組用于在預(yù)發(fā)環(huán)境中安全快速的驗(yàn)證用戶變更的工具。我們從根本上改變了隔離模型的方法:在共享環(huán)境中隔離請求,而不是提供完全隔離的環(huán)境。其核心是,我們允許用戶重寫通過預(yù)發(fā)環(huán)境的請求,并有條件的執(zhí)行實(shí)驗(yàn)代碼。大致的工作流程如下:

  1. 在預(yù)發(fā)環(huán)境上創(chuàng)建一個(gè)不向服務(wù)發(fā)現(xiàn)注冊的新部署,就是我們所說的卸載部署(offloaded deployment),并且保證向這個(gè)服務(wù)發(fā)出請求的其他用戶不會被路由到這個(gè)(可能被破壞的)實(shí)例。

  2. 基礎(chǔ)架構(gòu)應(yīng)該知道如何解釋在請求頭中嵌入的覆蓋信息,從而確保覆蓋元數(shù)據(jù)(override metadata)在整個(gè)調(diào)用圖(request call graph)中傳播。

  3. 修改每個(gè)服務(wù)的路由規(guī)則,從而可以利用請求頭中提供的覆蓋信息,根據(jù)覆蓋元數(shù)據(jù)指定的規(guī)則,路由到對應(yīng)的卸載部署(offloaded deployment)。

示例場景

假設(shè)一個(gè)用戶想要在端到端場景中測試新版本的onboarding服務(wù)。之前基于Onebox,用戶可以啟動Lyft堆棧的整個(gè)副本,并修改相應(yīng)服務(wù),以驗(yàn)證是否如預(yù)期般工作。

如今在預(yù)發(fā)環(huán)境中,用戶可以共享環(huán)境,但可以替換已卸載的實(shí)例,這些實(shí)例不會影響到正常的預(yù)發(fā)流量。

典型用戶向預(yù)發(fā)環(huán)境發(fā)出的請求不會通過任何被實(shí)時(shí)卸載的實(shí)例

通過給請求附加特定的頭("request baggage"),用戶可以選擇將請求路由到新實(shí)例:

頭部元數(shù)據(jù)允許用戶在每個(gè)請求的基礎(chǔ)上修改調(diào)用流

在本文的其余部分,我們將深入探討如何構(gòu)建這些組件來提供集成調(diào)試體驗(yàn)。

卸載部署(Offloaded Deployments)

Lyft使用Envoy作為服務(wù)網(wǎng)絡(luò)代理,處理眾多服務(wù)之間的通信

在Lyft,每項(xiàng)服務(wù)的每個(gè)實(shí)例都被部署在一個(gè)Envoy[4] sidecar旁邊,作為該服務(wù)的唯一出入口。通過確保所有網(wǎng)絡(luò)流量都通過Envoy,我們?yōu)殚_發(fā)人員提供了一個(gè)簡化的流量視圖,該視圖以一種與語言無關(guān)的方式提供了服務(wù)抽象、可觀察性和可擴(kuò)展性。

服務(wù)通過向其Envoy sidecar發(fā)送請求來調(diào)用上游[5]服務(wù),Envoy將請求轉(zhuǎn)發(fā)到上游的健康實(shí)例。我們通過控制平面更新Envoy的配置,控制平面基于Kubernetes事件通過xDS API[6]進(jìn)行更新。

避免服務(wù)發(fā)現(xiàn)

如果我們希望創(chuàng)建一個(gè)不會從網(wǎng)格中正常獲取服務(wù)流量的實(shí)例,我們需要指示控制平面將其排除在服務(wù)發(fā)現(xiàn)之外。為了實(shí)現(xiàn)這一點(diǎn),我們在Kubernetes pod標(biāo)簽中嵌入額外的信息,表示該pod已被卸載:

...
app=foo
environment=staging
offloaded-deploy=true
...

然后我們可以修改控制平面來過濾這些實(shí)例,確保它們在準(zhǔn)備階段不接收標(biāo)準(zhǔn)流量。

當(dāng)用戶準(zhǔn)備在預(yù)發(fā)環(huán)境(本地迭代后)創(chuàng)建卸載部署時(shí),首先必須在Github中創(chuàng)建一個(gè)pull request。我們的持續(xù)集成將自動啟動部署所需的容器鏡像構(gòu)建。然后用戶可以利用Github機(jī)器人顯式的卸載部署他們的服務(wù)到預(yù)發(fā)環(huán)境:

我們的Github機(jī)器人可以從PR簡單的創(chuàng)建一個(gè)卸載部署

通過這一方式,用戶可以為某個(gè)服務(wù)創(chuàng)建獨(dú)立的部署,與普通的臨時(shí)部署共享完全相同的環(huán)境:與標(biāo)準(zhǔn)數(shù)據(jù)庫的交互,出口調(diào)用到其他服務(wù),并且可以被標(biāo)準(zhǔn)的指標(biāo)/日志/分析系統(tǒng)所觀測。對于那些只想ssh到實(shí)例并測試腳本或運(yùn)行調(diào)試器而不擔(dān)心影響預(yù)發(fā)環(huán)境其余部分的開發(fā)人員來說,這被證明非常有用。然而,當(dāng)開發(fā)人員可以在手機(jī)上打開Lyft應(yīng)用,并確保請求在一個(gè)卸載部署中得到PR代碼的服務(wù)時(shí),卸載部署才真正具有威力。

覆蓋報(bào)頭和上下文傳播(Override Headers and Context Propagation)

要將請求路由到已卸載的部署,需要在請求中嵌入元數(shù)據(jù),以便通知基礎(chǔ)設(shè)施何時(shí)修改調(diào)用流。元數(shù)據(jù)包含想要覆蓋的服務(wù)的路由規(guī)則,以及應(yīng)該將流量引導(dǎo)到哪些卸載的部署上去。我們決定將這些元數(shù)據(jù)攜帶在請求頭中,從而對服務(wù)和服務(wù)所有者保持透明。

不過,我們需要確保頭信息可以通過使用不同語言編寫的服務(wù)在網(wǎng)格中傳播。我們已經(jīng)使用OpenTracing報(bào)頭(x-ot-span-context)將跟蹤信息從一個(gè)請求傳播到下一個(gè)請求。OpenTracing有一個(gè)叫做“baggage[7]”的概念,這是一個(gè)嵌在跨服務(wù)邊界的報(bào)頭中的持久化鍵/值結(jié)構(gòu)。將元數(shù)據(jù)編碼到baggage中,通過請求和跟蹤庫將其從一個(gè)請求傳播到下一個(gè)請求,使我們能夠進(jìn)行快速的處理。

構(gòu)造和附加Baggage

實(shí)際的HTTP報(bào)頭是一個(gè)base64編碼的trace protobuf[8]。我們創(chuàng)建了自己的protobuf,命名為Overrides,注入到跟蹤的baggage中,如下代碼演示:

syntax = "proto3";

/* container for override metadata */
message Overrides {
  
  // maps cluster_name -> ip_port
  map<string, string> routing_overrides = 1;
}

\small{我們可以將樣本數(shù)據(jù)結(jié)構(gòu)嵌入到跟蹤baggage中}

from base64 import standard_b64decode, standard_b64encode

from flask import Flask, request
from lightstep.lightstep_carrier_pb2 import BinaryCarrier

import overrides_pb2


def header_from_overrides(overrides: overrides_pb2.Overrides) -> bytes:
    """
    Attach the `overrides` to the trace's baggage and return the new `x-ot-span-context` header
    """

    # decode the trace from the current request context
    header = request.headers.get('x-ot-span-context', '')
    trace_proto = BinaryCarrier()
    trace_proto.ParseFromString(standard_b64decode(header))

    # b64encode the provided custom `overrides` and place in the baggage
    b64_overrides = standard_b64encode(overrides.SerializeToString())
    trace_proto.basic_ctx.baggage_items['overrides'] = b64_overrides

    # re-encode the modified trace for use as an outgoing HTTP header
    return standard_b64encode(trace_proto.SerializeToString())


# create a sample `Overrides` proto that overrides routing for `users` service
overrides_proto = overrides_pb2.Overrides()
overrides_proto.routing_overrides["users"] = "10.0.0.42:80"

with Flask(__name__).test_request_context('/add-baggage'):
    new_header_with_baggage = header_from_overrides(overrides_proto)
    print({"x-ot-span-context": new_header_with_baggage})
    # {'x-ot-span-context': b'Ei8iLQoJb3ZlcnJpZGVzEiBDaFVLQlhWelpYSnpFZ3d4TUM0d0xqQXVOREk2T0RBPQ=='}

\small{如何提取當(dāng)前的trace并覆蓋}

為了從開發(fā)人員那里抽象出這種數(shù)據(jù)序列化,我們?yōu)楝F(xiàn)有的代理應(yīng)用程序添加了創(chuàng)建頭的工具(請閱讀更多關(guān)于代理的信息)。開發(fā)人員將客戶端指向代理,從而可以用用戶定義的Typescript代碼攔截請求/響應(yīng)數(shù)據(jù)。我們創(chuàng)建了一個(gè)助手函數(shù)setEnvoyOverride(service: string, sha: string),它將通過sha查找IP地址,創(chuàng)建Override protobuf,編碼頭部,并確保它被附加到通過代理的每個(gè)請求上。

上下文傳播(Context Propagation)

上下文傳播在任何一個(gè)分布式跟蹤系統(tǒng)中都很重要。我們需要元數(shù)據(jù)在請求的整個(gè)生命周期內(nèi)都可用,以確保許多深層調(diào)用的服務(wù)能夠訪問用戶指定的覆蓋。我們希望確保每個(gè)服務(wù)都能將元數(shù)據(jù)正確轉(zhuǎn)發(fā)到請求流中后續(xù)的服務(wù)中——即使服務(wù)本身并不關(guān)心其內(nèi)容。

調(diào)用圖中的每個(gè)服務(wù)都必須傳播元數(shù)據(jù)以實(shí)現(xiàn)完整的跟蹤覆蓋

Lyft的基礎(chǔ)設(shè)施用我們最常用的語言(Python、Go、Typescript)維護(hù)標(biāo)準(zhǔn)的請求庫,為開發(fā)人員處理上下文傳播。如果服務(wù)所有者使用這些庫調(diào)用另一個(gè)服務(wù),上下文傳播對用戶就是透明的。

不幸的是,在這個(gè)項(xiàng)目推出期間,我們發(fā)現(xiàn)上下文傳播并不像我們希望的那樣普遍。最初經(jīng)常有用戶來找我們,說他們的請求沒有被覆蓋,罪魁禍?zhǔn)淄ǔJ莟race丟失。我們投入了大量資金,以確保上下文傳播能夠跨各種語言特性(例如Python gevent/greenlets)、多種請求協(xié)議(HTTP/gRPC)以及各種異步作業(yè)/隊(duì)列(SQS[9])工作。我們還添加了可觀察性和工具來診斷涉及trace丟失的問題,例如標(biāo)識沒有添加頭部的服務(wù)出口的指示板。

擴(kuò)展Envoy

既然我們已經(jīng)在請求中傳播了重寫元數(shù)據(jù),就需要修改網(wǎng)絡(luò)層來讀取元數(shù)據(jù)并重定向到想要的卸載實(shí)例。

因?yàn)槲覀兯蟹?wù)都是通過Envoy sidecar發(fā)出請求的,所以可以在這些代理中嵌入一些中間件來讀取覆蓋并適當(dāng)修改路由規(guī)則。我們利用Envoy的HTTP過濾系統(tǒng)[10]處理請求,因此在HTTP過濾器中實(shí)現(xiàn)了兩個(gè)步驟:讀取請求頭的覆蓋信息,并修改路由規(guī)則,從而將路由重定向到已卸載的部署。

利用Envoy HTTP過濾器跟蹤

我們決定創(chuàng)建一個(gè)解碼過濾器[10],允許我們在請求被發(fā)送到上游集群之前解析并對覆蓋做出反應(yīng)。HTTP過濾系統(tǒng)提供了簡單的API,可以獲取當(dāng)前的目的路由以及正在處理的請求的所有報(bào)頭。雖然是用C++實(shí)現(xiàn)的,但下面的偽代碼反映了基本要點(diǎn):

def routing_overrides_filter(route, headers):
    routing_overrides = headers.trace().baggage()['overrides'] # {'users': '10.0.0.42:80'}
    next_cluster = route.cluster() # 'users'

    # modify the route if there's an override for the cluster we are going to
    if next_cluster in routing_overrides:
        
        # the user provided the ip/port of their offloaded deploy in the header baggage
        offloaded_instance_ip_port = routing_overrides[next_cluster] # '10.0.0.42:80'
        
        # redirect the request to the ORIGINAL_DST cluster with the new ip/port header
        headers.set('x-envoy-original-dst-host', ip_port)
        route.set_cluster('original_dst_cluster')

過濾器使用Envoy的跟蹤實(shí)用程序提取baggage中包含的覆蓋。雖然過濾器總是可以訪問像traceIdisSampled這樣的跟蹤信息,但我們首先必須修改Envoy,從而可以提取baggage中的信息[11]。合并了這個(gè)更改后,過濾器就可以使用新的API來提取底層trace中的baggage:routing_overrides = headers.trace().baggage()['overrides']

最初目的集群(Original Destination Cluster)

假設(shè)覆蓋應(yīng)用于當(dāng)前目標(biāo)集群,則必須將請求重定向到已卸載的部署。我們使用Envoy的原始目的地[12](ORIGINAL_DST)將請求發(fā)送到一個(gè)由baggage提供的覆蓋。

對于我們配置的ORIGINAL_DST集群,最終目的地是由一個(gè)特殊的x-envoy-original-dst-host[13]報(bào)頭決定的,它包含一個(gè)ip/port,如10.0.42:80,HTTP過濾器可以改變這個(gè)報(bào)頭來重定向請求。

例如,如果請求最初是為user集群準(zhǔn)備的,但是用戶重寫了ip/port,我們將把x-envoy-original-dst-host更改為所提供的ip/port。

當(dāng)x-envoy-original-dst-host被修改后,過濾器需要將請求發(fā)送到ORIGINAL_DST集群,以確保發(fā)送到新的目的地。這一需求促使我們對Envoy做出了第二個(gè)變更:支持路由可變性[14]。合并此變更后,過濾器就可以改變目標(biāo)集群:route.set_cluster('original_dst_cluster')。

結(jié)果

通過卸載部署、傳播baggage和Envoy過濾器,我們現(xiàn)在已經(jīng)展示了預(yù)發(fā)覆蓋??的所有主要組件。

這個(gè)工作流程極大改進(jìn)了端到端測試的開銷。我們現(xiàn)在每個(gè)月都有100個(gè)獨(dú)立的服務(wù)部署,與以前的Onebox解決方案相比,預(yù)發(fā)覆蓋有以下優(yōu)點(diǎn):

  • 環(huán)境配置: Onebox要求用戶啟動數(shù)百個(gè)容器并運(yùn)行定制的種子腳本,需要開發(fā)人員花上至少1個(gè)小時(shí)準(zhǔn)備環(huán)境。通過預(yù)發(fā)覆蓋,用戶可以在10分鐘內(nèi)通過端到端環(huán)境部署某個(gè)變更。
  • 低成本基礎(chǔ)設(shè)施: Onebox運(yùn)行的是完全獨(dú)立于預(yù)發(fā)/生產(chǎn)環(huán)境的技術(shù)棧,所以底層基礎(chǔ)設(shè)施組件(例如網(wǎng)絡(luò)、可觀察性)通常是單獨(dú)實(shí)現(xiàn)的。通過將端到端測試轉(zhuǎn)移到預(yù)發(fā)環(huán)境,由于環(huán)境改進(jìn)為集中維護(hù),降低了基礎(chǔ)設(shè)施支持的成本。
  • 低成本功能驗(yàn)證: 由于Onebox和產(chǎn)品之間的差異,即使在Onebox端到端測試之后,用戶也經(jīng)常(合理的)懷疑代碼的正確性。預(yù)發(fā)與生產(chǎn)環(huán)境在數(shù)據(jù)和流量模式方面更為接近,使用戶更有信心相信如果變更在預(yù)發(fā)環(huán)境中就緒,也就意味著在生產(chǎn)環(huán)境中就緒。
額外的工作

啟動預(yù)發(fā)覆蓋是一項(xiàng)涉及網(wǎng)絡(luò)、部署、可觀察性、安全性和開發(fā)工具的跨組織工作。以下是一些沒有涉及到的額外工作流程:

  • 配置覆蓋: 除了在baggage中指定路由覆蓋外,我們還允許用戶在每個(gè)請求的基礎(chǔ)上修改配置變量。通過修改配置庫,賦予baggage優(yōu)先級,讓用戶在啟用全局配置之前為請求設(shè)置特性標(biāo)志。
  • 安全影響: 因?yàn)榭梢灾付ǜ采w路由規(guī)則,所以必須鎖定過濾器功能,以確保不良行為者不能被任意路由。
未來的工作

展望未來,我們可以通過預(yù)發(fā)覆蓋做更多的事情,讓用戶重新創(chuàng)建想要驗(yàn)證的端到端場景:

  • 可共享的baggage: 為用戶提供一個(gè)集中管理的baggage存儲,允許持久化一組獨(dú)特的覆蓋(服務(wù)foo是X,服務(wù)bar是Y,標(biāo)簽baz是Z),通過與團(tuán)隊(duì)成員共享確切的場景而改善協(xié)作。
  • 覆蓋用例: 讓我們的基礎(chǔ)設(shè)施了解其他覆蓋,以便讓用戶控制請求的行為。例如,我們可以使用Envoy錯(cuò)誤注入[15]將人造延遲注入到請求中,臨時(shí)啟用調(diào)試日志記錄,或者重定向到不同的數(shù)據(jù)庫。
  • 與本地開發(fā)集成: 我們可以允許重寫請求,直接將請求重路由到用戶的筆記本電腦,而不是要求用戶在準(zhǔn)備階段啟動他們的PR實(shí)例。

請繼續(xù)關(guān)注我們系列中的下一篇文章,我們將展示如何在交付階段使用自動化驗(yàn)收測試對生產(chǎn)部署進(jìn)行驗(yàn)收!

References:
[1] Scaling productivity on microservices at Lyft (Part 3): Extending our Envoy mesh with staging overrides: https://eng.lyft.com/scaling-productivity-on-microservices-at-lyft-part-3-extending-our-envoy-mesh-with-staging-fdaafafca82f
[2] Service Level Objective: https://en.wikipedia.org/wiki/Service-level_objective
[3] Severity Levels: https://response.pagerduty.com/before/severity_levels/
[4] Envoy Proxy: https://www.envoyproxy.io/
[5] Terminology: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/intro/terminology#:~:text=Upstream%3A%20An%20upstream%20host%20receives%20connections%20and%20requests%20from%20Envoy%20and%20returns%20responses.
[6] xDS protocol: https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol
[7] Tags, logs and baggage: https://opentracing.io/docs/overview/tags-logs-baggage/
[8] lightstep_carrier.proto: https://github.com/lightstep/lightstep-tracer-protos/blob/13cdeec9bd4a0ba2cd7062fecde3a057071edcb8/src/lightstep/lightstep_carrier.proto
[9] Amazon Simple Queue Service: https://aws.amazon.com/sqs/
[10] HTTP filters: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/http/http_filters
[11] tracing: add baggage methods to Tracing::Span: https://github.com/envoyproxy/envoy/pull/12260
[12] Original destination: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/original_dst
[13] Original destination host request header: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/original_dst#original-destination-host-request-header
[14] http: support route mutability: https://github.com/envoyproxy/envoy/pull/15266
[15] Fault Injection: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/fault_filter

你好,我是俞凡,在Motorola做過研發(fā),現(xiàn)在在Mavenir做技術(shù)工作,對通信、網(wǎng)絡(luò)、后端架構(gòu)、云原生、DevOps、CICD、區(qū)塊鏈、AI等技術(shù)始終保持著濃厚的興趣,平時(shí)喜歡閱讀、思考,相信持續(xù)學(xué)習(xí)、終身成長,歡迎一起交流學(xué)習(xí)。
微信公眾號:DeepNoMind

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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