Spring Cloud 系列之 Sleuth 鏈路追蹤(一)

隨着微服務架構的流行,服務按照不同的維度進行拆分,一次請求往往需要涉及到多個服務。互聯網應用構建在不同的軟件模塊集上,這些軟件模塊,有可能是由不同的團隊開發、可能使用不同的編程語言來實現、有可能布在了幾千臺服務器,橫跨多個不同的數據中心。因此,就需要一些可以幫助理解系統行爲、用於分析性能問題的工具,以便發生故障的時候,能夠快速定位和解決問題。在複雜的微服務架構系統中,幾乎每一個前端請求都會形成一個複雜的分佈式服務調用鏈路。一個請求完整調用鏈可能如下圖所示:

隨着服務的越來越多,對調用鏈的分析會越來越複雜。它們之間的調用關係也許如下:

隨着業務規模不斷增大、服務不斷增多以及頻繁變更的情況下,面對複雜的調用鏈路就帶來一系列問題:

  • 如何快速發現問題?
  • 如何判斷故障影響範圍?
  • 如何梳理服務依賴以及依賴的合理性?
  • 如何分析鏈路性能問題以及實時容量規劃?

而鏈路追蹤的出現正是爲了解決這種問題,它可以在複雜的服務調用中定位問題,還可以在新人加入後臺團隊之後,讓其清楚地知道自己所負責的服務在哪一環。

除此之外,如果某個接口突然耗時增加,也不必再逐個服務查詢耗時情況,我們可以直觀地分析出服務的性能瓶頸,方便在流量激增的情況下精準合理地擴容。

什麼是鏈路追蹤

“鏈路追蹤”一詞是在 2010 年提出的,當時谷歌發佈了一篇 Dapper 論文:Dapper,大規模分佈式系統的跟蹤系統,介紹了谷歌自研的分佈式鏈路追蹤的實現原理,還介紹了他們是怎麼低成本實現對應用透明的。

單純的理解鏈路追蹤,就是指一次任務的開始到結束,期間調用的所有系統及耗時(時間跨度)都可以完整記錄下來。

其實 Dapper 一開始只是一個獨立的調用鏈路追蹤系統,後來逐漸演化成了監控平臺,並且基於監控平臺孕育出了很多工具,比如實時預警、過載保護、指標數據查詢等。

除了谷歌的 Dapper,還有一些其他比較有名的產品,比如阿里的鷹眼、大衆點評的 CAT、Twitter 的 Zipkin、Naver(著名社交軟件LINE的母公司)的 PinPoint 以及國產開源的 SkyWalking(已貢獻給 Apache) 等。

什麼是 Sleuth

Spring Cloud Sleuth 爲 Spring Cloud 實現了分佈式跟蹤解決方案。兼容 Zipkin,HTrace 和其他基於日誌的追蹤系統,例如 ELK(Elasticsearch 、Logstash、 Kibana)。

Spring Cloud Sleuth 提供了以下功能:

  • 鏈路追蹤:通過 Sleuth 可以很清楚的看出一個請求都經過了那些服務,可以很方便的理清服務間的調用關係等。
  • 性能分析:通過 Sleuth 可以很方便的看出每個採樣請求的耗時,分析哪些服務調用比較耗時,當服務調用的耗時隨着請求量的增大而增大時, 可以對服務的擴容提供一定的提醒。
  • 數據分析,優化鏈路:對於頻繁調用一個服務,或並行調用等,可以針對業務做一些優化措施。
  • 可視化錯誤:對於程序未捕獲的異常,可以配合 Zipkin 查看。

專業術語

點擊鏈接觀看:Sleuth 專業術語視頻(獲取更多請關注公衆號「哈嘍沃德先生」)

Span

基本工作單位,一次單獨的調用鏈可以稱爲一個 Span,Dapper 記錄的是 Span 的名稱,以及每個 Span 的 ID 和父 ID,以重建在一次追蹤過程中不同 Span 之間的關係,圖中一個矩形框就是一個 Span,前端從發出請求到收到回覆就是一個 Span。

開始跟蹤的初始跨度稱爲root span。該跨度的 ID 的值等於跟蹤 ID。

Dapper 記錄了 span 名稱,以及每個 span 的 ID 和父 span ID,以重建在一次追蹤過程中不同 span 之間的關係。如果一個 span 沒有父 ID 被稱爲 root span。所有 span 都掛在一個特定的 Trace 上,也共用一個 trace id。

Trace

一系列 Span 組成的樹狀結構,一個 Trace 認爲是一次完整的鏈路,內部包含 n 多個 Span。Trace 和 Span 存在一對多的關係,Span 與 Span 之間存在父子關係。

舉個例子:客戶端調用服務 A 、服務 B 、服務 C 、服務 F,而每個服務例如 C 就是一個 Span,如果在服務 C 中另起線程調用了 D,那麼 D 就是 C 的子 Span,如果在服務 D 中另起線程調用了 E,那麼 E 就是 D 的子 Span,這個 C -> D -> E 的鏈路就是一條 Trace。如果鏈路追蹤系統做好了,鏈路數據有了,藉助前端解析和渲染工具,可以達到下圖中的效果:

Annotation

用來及時記錄一個事件的存在,一些核心 annotations 用來定義一個請求的開始和結束。

  • cs - Client Sent:客戶端發起一個請求,這個 annotation 描述了這個 span 的開始;
  • sr - Server Received:服務端獲得請求並準備開始處理它,如果 sr 減去 cs 時間戳便可得到網絡延遲;
  • ss - Server Sent:請求處理完成(當請求返回客戶端),如果 ss 減去 sr 時間戳便可得到服務端處理請求需要的時間;
  • cr - Client Received:表示 span 結束,客戶端成功接收到服務端的回覆,如果 cr 減去 cs 時間戳便可得到客戶端從服務端獲取回覆的所有所需時間。

實現原理

首先感謝張以諾製作的實現原理圖。

如果想知道一個接口在哪個環節出現了問題,就必須清楚該接口調用了哪些服務,以及調用的順序,如果把這些服務串起來,看起來就像鏈條一樣,我們稱其爲調用鏈。

想要實現調用鏈,就要爲每次調用做個標識,然後將服務按標識大小排列,可以更清晰地看出調用順序,我們暫且將該標識命名爲 spanid。

實際場景中,我們需要知道某次請求調用的情況,所以只有 spanid 還不夠,得爲每次請求做個唯一標識,這樣才能根據標識查出本次請求調用的所有服務,而這個標識我們命名爲 traceid。

現在根據 spanid 可以輕易地知道被調用服務的先後順序,但無法體現調用的層級關係,正如下圖所示,多個服務可能是逐級調用的鏈條,也可能是同時被同一個服務調用。

所以應該每次都記錄下是誰調用的,我們用 parentid 作爲這個標識的名字。

到現在,已經知道調用順序和層級關係了,但是接口出現問題後,還是不能找到出問題的環節,如果某個服務有問題,那個被調用執行的服務一定耗時很長,要想計算出耗時,上述的三個標識還不夠,還需要加上時間戳,時間戳可以更精細一點,精確到微秒級。

只記錄發起調用時的時間戳還算不出耗時,要記錄下服務返回時的時間戳,有始有終才能算出時間差,既然返回的也記了,就把上述的三個標識都記一下吧,不然區分不出是誰的時間戳。

雖然能計算出從服務調用到服務返回的總耗時,但是這個時間包含了服務的執行時間和網絡延遲,有時候我們需要區分出這兩類時間以方便做針對性優化。那如何計算網絡延遲呢?我們可以把調用和返回的過程分爲以下四個事件。

  • Client Sent 簡稱 cs,客戶端發起調用請求到服務端。
  • Server Received 簡稱 sr,指服務端接收到了客戶端的調用請求。
  • Server Sent 簡稱 ss,指服務端完成了處理,準備將信息返給客戶端。
  • Client Received 簡稱 cr,指客戶端接收到了服務端的返回信息。

假如在這四個事件發生時記錄下時間戳,就可以輕鬆計算出耗時,比如 sr 減去 cs 就是調用時的網絡延遲,ss 減去 sr 就是服務執行時間,cr 減去 ss 就是服務響應的延遲,cr 減 cs 就是整個服務調用執行的時間。

其實 span 內除了記錄這幾個參數之外,還可以記錄一些其他信息,比如發起調用服務名稱、被調服務名稱、返回結果、IP、調用服務的名稱等,最後,我們再把相同 parentid 的 span 信息合成一個大的 span 塊,就完成了一個完整的調用鏈。

環境準備

sleuth-demo 聚合工程。SpringBoot 2.2.4.RELEASESpring Cloud Hoxton.SR1

  • eureka-server:註冊中心
  • eureka-server02:註冊中心
  • gateway-server:Spring Cloud Gateway 服務網關
  • product-service:商品服務,提供了根據主鍵查詢商品接口 http://localhost:7070/product/{id} 根據多個主鍵查詢商品接口 http://localhost:7070/product/listByIds
  • order-service:訂單服務,提供了根據主鍵查詢訂單接口 http://localhost:9090/order/{id} 且訂單服務調用商品服務。

入門案例

點擊鏈接觀看:Sleuth 入門案例視頻(獲取更多請關注公衆號「哈嘍沃德先生」)

添加依賴

在需要進行鏈路追蹤的項目中(服務網關、商品服務、訂單服務)添加 spring-cloud-starter-sleuth 依賴。

<!-- spring cloud sleuth 依賴 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

記錄日誌

在需要鏈路追蹤的項目中添加 logback.xml 日誌文件,內容如下(logback 日誌的輸出級別需要是 DEBUG 級別):

注意修改 <property name="log.path" value="${catalina.base}/gateway-server/logs"/> 中項目名稱。

日誌核心配置:%d{yyyy-MM-dd HH:mm:ss.SSS} [${applicationName},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-}] [%thread] %-5level %logger{50} - %msg%n

<?xml version="1.0" encoding="UTF-8"?>
<!-- 日誌級別從低到高分爲TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果設置爲WARN,則低於WARN的信息都不會輸出 -->
<!-- scan: 當此屬性設置爲true時,配置文件如果發生改變,將會被重新加載,默認值爲true -->
<!-- scanPeriod: 設置監測配置文件是否有修改的時間間隔,如果沒有給出時間單位,默認單位是毫秒。當scan爲true時,此屬性生效。默認的時間間隔爲1分鐘。 -->
<!-- debug: 當此屬性設置爲true時,將打印出logback內部日誌信息,實時查看logback運行狀態。默認值爲false。 -->
<configuration scan="true" scanPeriod="10 seconds">
    <!-- 日誌上下文名稱 -->
    <contextName>my_logback</contextName>
    <!-- name的值是變量的名稱,value的值是變量定義的值。通過定義的值會被插入到logger上下文中。定義變量後,可以使“${}”來使用變量。 -->
    <property name="log.path" value="${catalina.base}/gateway-server/logs"/>
    <!-- 加載 Spring 配置文件信息 -->
    <springProperty scope="context" name="applicationName" source="spring.application.name" defaultValue="localhost"/>
    <!-- 日誌輸出格式 -->
    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [${applicationName},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-}] [%thread] %-5level %logger{50} - %msg%n"/>

    <!--輸出到控制檯-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日誌appender是爲開發使用,只配置最底級別,控制檯輸出的日誌級別是大於或等於此級別的日誌信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>DEBUG</level>
        </filter>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <!-- 設置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 輸出到文件 -->
    <!-- 時間滾動輸出 level爲 DEBUG 日誌 -->
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在記錄的日誌文件的路徑及文件名 -->
        <file>${log.path}/log_debug.log</file>
        <!--日誌文件輸出格式-->
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset> <!-- 設置字符集 -->
        </encoder>
        <!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日誌歸檔 -->
            <fileNamePattern>${log.path}/debug/log-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日誌文件保留天數-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日誌文件只記錄debug級別的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 時間滾動輸出 level爲 INFO 日誌 -->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在記錄的日誌文件的路徑及文件名 -->
        <file>${log.path}/log_info.log</file>
        <!--日誌文件輸出格式-->
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日誌歸檔路徑以及格式 -->
            <fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日誌文件保留天數-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日誌文件只記錄info級別的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 時間滾動輸出 level爲 WARN 日誌 -->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在記錄的日誌文件的路徑及文件名 -->
        <file>${log.path}/log_warn.log</file>
        <!--日誌文件輸出格式-->
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset> <!-- 此處設置字符集 -->
        </encoder>
        <!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- 每個日誌文件最大100MB -->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日誌文件保留天數-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日誌文件只記錄warn級別的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>WARN</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 時間滾動輸出 level爲 ERROR 日誌 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在記錄的日誌文件的路徑及文件名 -->
        <file>${log.path}/log_error.log</file>
        <!--日誌文件輸出格式-->
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset> <!-- 此處設置字符集 -->
        </encoder>
        <!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日誌文件保留天數-->
            <maxHistory>15</maxHistory>
            <!-- 日誌量最大 10 GB -->
            <totalSizeCap>10GB</totalSizeCap>
        </rollingPolicy>
        <!-- 此日誌文件只記錄ERROR級別的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 對於類路徑以 com.example.logback 開頭的Logger,輸出級別設置爲warn,並且只輸出到控制檯 -->
    <!-- 這個logger沒有指定appender,它會繼承root節點中定義的那些appender -->
    <!-- <logger name="com.example.logback" level="warn"/> -->

    <!--通過 LoggerFactory.getLogger("myLog") 可以獲取到這個logger-->
    <!--由於這個logger自動繼承了root的appender,root中已經有stdout的appender了,自己這邊又引入了stdout的appender-->
    <!--如果沒有設置 additivity="false" ,就會導致一條日誌在控制檯輸出兩次的情況-->
    <!--additivity表示要不要使用rootLogger配置的appender進行輸出-->
    <logger name="myLog" level="INFO" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <!-- 日誌輸出級別及方式 -->
    <root level="DEBUG">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="DEBUG_FILE"/>
        <appender-ref ref="INFO_FILE"/>
        <appender-ref ref="WARN_FILE"/>
        <appender-ref ref="ERROR_FILE"/>
    </root>

</configuration>

訪問

訪問:http://localhost:9000/order-service/order/1 ,結果如下:

服務網關打印信息:

[gateway-server,95aa725089b757f8,95aa725089b757f8]

商品服務打印信息

[product-service,95aa725089b757f8,e494e064842ce4e8]

訂單服務打印信息

[order-service,95aa725089b757f8,f4ee41a6dcf08717]

通過打印信息可以得知,整個鏈路的 traceId 爲:95aa725089b757f8spanId 爲:e494e064842ce4e8f4ee41a6dcf08717

查看日誌文件並不是一個很好的方法,當微服務越來越多日誌文件也會越來越多,查詢工作會變得越來越麻煩,Spring 官方推薦使用 Zipkin 進行鏈路跟蹤。Zipkin 可以將日誌聚合,並進行可視化展示和全文檢索。

使用 Zipkin 進行鏈路跟蹤

什麼是 Zipkin

Zipkin 是 Twitter 公司開發貢獻的一款開源的分佈式實時數據追蹤系統(Distributed Tracking System),基於 Google Dapper 的論文設計而來,其主要功能是聚集各個異構系統的實時監控數據。

它可以收集各個服務器上請求鏈路的跟蹤數據,並通過 Rest API 接口來輔助我們查詢跟蹤數據,實現對分佈式系統的實時監控,及時發現系統中出現的延遲升高問題並找出系統性能瓶頸的根源。除了面向開發的 API 接口之外,它還提供了方便的 UI 組件,每個服務向 Zipkin 報告計時數據,Zipkin 會根據調用關係生成依賴關係圖,幫助我們直觀的搜索跟蹤信息和分析請求鏈路明細。Zipkin 提供了可插拔數據存儲方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。

分佈式跟蹤系統還有其他比較成熟的實現,例如:Naver 的 PinPoint、Apache 的 HTrace、阿里的鷹眼 Tracing、京東的 Hydra、新浪的 Watchman,美團點評的 CAT,Apache 的 SkyWalking 等。

工作原理

共有四個組件構成了 Zipkin:

  • Collector:收集器組件,處理從外部系統發送過來的跟蹤信息,將這些信息轉換爲 Zipkin 內部處理的 Span 格式,以支持後續的存儲、分析、展示等功能。
  • Storage:存儲組件,處理收集器接收到的跟蹤信息,默認將信息存儲在內存中,可以修改存儲策略使用其他存儲組件,支持 MySQL,Elasticsearch 等。
  • Web UI:UI 組件,基於 API 組件實現的上層應用,提供 Web 頁面,用來展示 Zipkin 中的調用鏈和系統依賴關係等。
  • RESTful API:API 組件,爲 Web 界面提供查詢存儲中數據的接口。

Zipkin 分爲兩端,一個是 Zipkin 服務端,一個是 Zipkin 客戶端,客戶端也就是微服務的應用,客戶端會配置服務端的 URL 地址,一旦發生服務間的調用的時候,會被配置在微服務裏面的 Sleuth 的監聽器監聽,並生成相應的 Trace 和 Span 信息發送給服務端。發送的方式有兩種,一種是消息總線的方式如 RabbitMQ 發送,還有一種是 HTTP 報文的方式發送。

服務端部署

服務端是一個獨立的可執行的 jar 包,官方下載地址:https://search.maven.org/remote_content?g=io.zipkin&a=zipkin-server&v=LATEST&c=exec,使用 java -jar zipkin.jar 命令啓動,端口默認爲 9411。我們下載的 jar 包爲:zipkin-server-2.20.1-exec.jar,啓動命令如下:

java -jar zipkin-server-2.20.1-exec.jar

訪問:http://localhost:9411/ 結果如下:

目前最新版界面。

之前舊版本界面。

客戶端部署

點擊鏈接觀看:Zipkin 客戶端部署視頻(獲取更多請關注公衆號「哈嘍沃德先生」)

添加依賴

在需要進行鏈路追蹤的項目中(服務網關、商品服務、訂單服務)添加 spring-cloud-starter-zipkin 依賴。

<!-- spring cloud zipkin 依賴 -->
<dependency> 
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

配置文件

在需要進行鏈路追蹤的項目中(服務網關、商品服務、訂單服務)配置 Zipkin 服務端地址及數據傳輸方式。默認即如下配置。

spring:
  zipkin:
    base-url: http://localhost:9411/ # 服務端地址
    sender:
      type: web                      # 數據傳輸方式,web 表示以 HTTP 報文的形式向服務端發送數據
  sleuth:
    sampler:
      probability: 1.0               # 收集數據百分比,默認 0.1(10%)

訪問

訪問:http://localhost:9000/order-service/order/1 結果如下:

新版操作如下:

訪問:http://localhost:9411/ 根據時間過濾點擊搜索結果如下:

點擊對應的追蹤信息可查看請求鏈路詳細。

通過依賴可以查看鏈路中服務的依賴關係。

舊版操作如下:

訪問:http://localhost:9411/ 點擊查找結果如下:

點擊對應的追蹤信息可查看請求鏈路詳細。

通過依賴可以查看鏈路中服務的依賴關係。

Zipkin Server 默認存儲追蹤數據至內存中,這種方式並不適合生產環境,一旦 Server 關閉重啓或者服務崩潰,就會導致歷史數據消失。Zipkin 支持修改存儲策略使用其他存儲組件,支持 MySQL,Elasticsearch 等。

下一篇我們講解 Sleuth 基於 Zipkin 存儲鏈路追蹤數據至 MySQL,Elasticsearch 以及使用 MQ 存儲鏈路追蹤數據至 MySQL,Elasticsearch,記得關注噢~

本文采用 知識共享「署名-非商業性使用-禁止演繹 4.0 國際」許可協議

大家可以通過 分類 查看更多關於 Spring Cloud 的文章。


🤗 您的點贊轉發是對我最大的支持。

📢 掃碼關注 哈嘍沃德先生「文檔 + 視頻」每篇文章都配有專門視頻講解,學習更輕鬆噢 ~


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章