一、基本概念
1. 發(fā)布與訂閱消息系統(tǒng)
數(shù)據(jù)(消息)的發(fā)送者(發(fā)布者)不會直接把消息發(fā)送給接收者。發(fā)布者以某種方式對消息進行分類,接收者(訂閱者)訂閱它們,以便接收特定類型的消息。發(fā)布與訂閱系統(tǒng)一般會有一個broker,即發(fā)布消息的中心點。
2. 消息和批次
Kafka的數(shù)據(jù)單元被稱為消息,由字節(jié)數(shù)組組成,消息有一個可選的元數(shù)據(jù),也就是鍵。當需要控制消息寫入特定的分區(qū)時,可以指定消息的鍵,最簡單的例子是為鍵生成一個一致性散列值,然后使用散列值對主題分區(qū)數(shù)進行取模,為消息選取分區(qū),這樣可以保證相同鍵的消息總是被寫到相同的分區(qū)上。
為了提高效率,消息被分批次寫入到kafka。批次就是一組消息,這些消息同屬于一個主題和分區(qū)。批次消息可以減少網(wǎng)絡開銷,也可以被壓縮。
3. 主題和分區(qū)
kafka的消息通過主題進行分類。主題可以被分為若干個分區(qū),一個分區(qū)就是一個提交日志(Commit Log)。消息以追加的方式寫入分區(qū),然后以先入先出(FIFO)的順序讀取。由于一個主題一般包含多個分區(qū),因此無法在整個主題范圍內(nèi)保證消息的順序,但可以保證消息在單個分區(qū)內(nèi)的順序。
Kafka通過分區(qū)來實現(xiàn)數(shù)據(jù)冗余和伸縮性。分區(qū)可以分布在不同的物理服務器上。

4. 生產(chǎn)者和消費者
生產(chǎn)者創(chuàng)建消息。一個消息會被發(fā)布到一個指定的主題上。生產(chǎn)者在默認情況下(不指定消息的鍵)吧消息均衡地發(fā)布到主題的所有分區(qū)上。也可以通過消息鍵和分區(qū)器來實現(xiàn)將消息直接寫到指定的分區(qū),分區(qū)器為鍵生成一個散列值,并將其映射到指定的分區(qū)上。
消費者讀取消息。消費者訂閱一個或多個主題,并按照消息生成的順序讀取它們。消費者通過檢查消息的偏移量來區(qū)分已經(jīng)讀過的消息。
偏移量是消息的元數(shù)據(jù),是一個不斷遞增的整數(shù)值,在創(chuàng)建消息時,kafka會把它添加到消息里。在同一個分區(qū)里,每個消息的偏移量都是唯一的。消費者把每個分區(qū)的消息偏移量保存在Zookeeper或kafka上。(節(jié)點路徑:/consumers/{group_id}/offsets/{topic}/{broker_id}-{partition_id})
消費者是消費者群組的一部分,若干個消費者共同讀取一個主題。消費者組保證每個分區(qū)只能被一個消費者使用。如果一個消費者失效,群組里的其他消費者可以接管失效消費者的工作。消費者組里的消費者平均地讀取固定的分區(qū),多于分區(qū)數(shù)量的消費者將會被閑置。

5. broker和集群
一個獨立的Kafka服務器被稱為broker。broker接收來自生產(chǎn)者的消息,為消息設置偏移量,并提交消息到磁盤保存。broker為消費者提供服務,對讀取分區(qū)的請求作出響應,返回已經(jīng)提交到磁盤上的消息。
broker是集群的組成部分。每個集群都有一個broker同時充當了集群控制器的角色(自動從集群的活躍成員中選舉出來)??刂破髫撠煿芾砉ぷ?,包括將分區(qū)分配給broker和監(jiān)控broker。
在集群中,一個分區(qū)從屬與一個broker,該broker被稱為分區(qū)的首領。一個分區(qū)可以分配給多個broker,這時候會發(fā)生分區(qū)復制。也就是說,一個主題的同一分區(qū)會存在于集群的所有broker上,其中一個活躍可用的分區(qū)作為首領,其余的作為副本。如果有一個broker失效,其他broker可以接管領導權。
首領副本負責所有客戶端讀寫操作(包括生產(chǎn)者和消費者),跟隨者副本僅僅從首領副本同步數(shù)據(jù)。當首領副本出現(xiàn)故障是,跟隨者副本中的一個副本會被選擇為新的首領副本。

因為每個分區(qū)的副本中只有首領副本接收讀寫,所以每個服務端都會作為某些分區(qū)的首領副本,以及另外一些分區(qū)的跟隨者副本,這樣Kafka集群的所有服務端整體上對客戶端是負載均衡的
6. 消息模型
推送模型(Push)
基于推送模型的消息系統(tǒng),由消息代理(broker)記錄消費者的消息狀態(tài)。消息代理在將消息推送到消費者后,將這條消息標記為已消費,但這種方式無法很好地保證消息的處理語義
拉取模型(Pull)
拉取模型由消費者自己記錄消費狀態(tài),每個消費者互相獨立地順序讀取每個分區(qū)的消息。消費者能拉取的最大上限通過最高水位(watermark)控制,生產(chǎn)者最新寫入的消息如果還沒有達到備份數(shù)量,對消費者是不可見的
二、Kafka的設計與實現(xiàn)
1. 文件系統(tǒng)的持久化與數(shù)據(jù)傳輸效率
- 預讀
提前將一個比較大的磁盤塊讀入內(nèi)存 - 后寫
將若干小的邏輯寫操作合并成一個大的物理寫操作 - 磁盤緩存
在內(nèi)存中盡量報錯盡可能多的數(shù)據(jù),并在需要時將這些數(shù)據(jù)刷新到磁盤 - 消息分組
用批量的方式一次發(fā)送一個消息組 - 壓縮消息集
-
零拷貝
使用零拷貝技術只需將磁盤文件的數(shù)據(jù)復制到頁面緩存中一次,然后將數(shù)據(jù)從頁面緩存中直接發(fā)送到網(wǎng)絡(發(fā)送給不同的使用者時,都可以重復使用同一個頁面緩存),避免了重復的復制操作
傳統(tǒng)的數(shù)據(jù)復制方法和優(yōu)化的零拷貝
2. 生產(chǎn)者與消費者
后面詳細敘述
3. 副本機制與容錯處理
Kafka的副本機制會在多個broker上對每個主題分區(qū)的日志進行復制。副本的單位是主題的分區(qū),每個主題的每個分區(qū)都有一個首領副本以及任意個跟隨者副本。
- 首領副本
所有的讀寫請求總是被路由到分區(qū)的首領副本上 - 跟隨者副本
跟隨者副本會和首領副本保持數(shù)據(jù)同步,在首領副本失效時替換為首領副本 - 節(jié)點存活
節(jié)點的存活定義的條件:1. 節(jié)點必須和Zookeeper保持會話;2. 如果這個節(jié)點是某個分區(qū)的跟隨者副本,它必須對分區(qū)首領副本的寫操作進行復制,并且復制的進度不能落后太多 - ISR
滿足上述兩個條件被稱為in-sync(正在同步中)。每個分區(qū)的首領副本會跟蹤in-sync的跟隨者副本節(jié)點(In Sync Replicas,即ISR)。如果一個跟隨者副本掛掉、沒有響應或落后太多,首領副本就會將其從同步副本中移除。反之,如果跟隨者副本重新趕上首領副本,他就會加入到首領副本的同步集合中 - 確認提交
一條消息只有被ISR集合中所有副本都保存到本地的日志文件中,才會被認為是成功提交了。任何時刻,只要ISR至少有一個副本是存活的,Kafka就可以保證“一條消息一旦被提交,就不會丟失”。只有已經(jīng)提交的消息才能被消費者消費。
三、Kafka生產(chǎn)者——向Kafka寫入數(shù)據(jù)
1. kafka發(fā)送消息的主要步驟

ProducerRecord需要包含目標主題和發(fā)送的內(nèi)容,還可以指定鍵或分區(qū)。在發(fā)送ProducerRecord對象時,生產(chǎn)者要先把鍵和值對象序列化成字節(jié)數(shù)組。
接下來,數(shù)據(jù)被傳給分區(qū)器。如果ProducerRecord對象里指定了分區(qū),直接返回指定的分區(qū),如果沒有則分區(qū)器會根據(jù)鍵和分區(qū)值來確定分區(qū)。然后這條記錄被添加到一個記錄批次里,這個批次里的所有消息會被發(fā)送到相同的主題和分區(qū)上。有一個獨立的線程負責發(fā)送批次消息到響應的broker上。
broker收到消息后會返回一個響應。如果消息成功寫入到Kafka,就返回一個RecordMetaData對象,包含了主題、分區(qū)信息和分區(qū)中的偏移量。如果失敗則返回錯誤,生產(chǎn)者收到錯誤之后會嘗試重新發(fā)送消息。
2. 發(fā)送消息的模式
- 發(fā)送并忘記
把消息發(fā)送給broker,但并不關心它是否被正常送達,不能保證消息發(fā)送的可靠性 - 同步發(fā)送
使用send()方法發(fā)送消息,它會返回一個Future對象,調(diào)用 get() 方法進行等待,來判斷消息是否成功發(fā)送 - 異步發(fā)送
使用send()方法并制定回調(diào)函數(shù),服務器在返回響應時調(diào)用該函數(shù)
四、Kafka消費者——從Kafka讀取數(shù)據(jù)
1. 消費者和消費者群組
Kafka消費者從屬于消費者群組,一個群組里的消費者訂閱的是同一個主題,每個消費者接收主題一部分分區(qū)的消息。消費者群組里的消費者總是平均地讀取固定的分區(qū)。多余分區(qū)數(shù)量的消費者將會閑置,不會收到任何消息。
一個主題可以被多個消費者群組讀取,這些消費者群組之間不會相互影響;一個消費者群組也可以訂閱多個主題。

2. 消費者群組和分區(qū)再均衡
再均衡的含義:分區(qū)的所有權從一個消費者轉(zhuǎn)移到另一個消費者
分區(qū)會再均衡的情況:
- 消費者加入群組
- 消費者離開群組
- 主題分區(qū)發(fā)生變化
消費者通過向北指派為群組協(xié)調(diào)器的broker(不同的群組可以有不同的協(xié)調(diào)器)發(fā)送心跳來維持它們和群組的從屬關系一級它們對分區(qū)的所有權關系。只要消費者以正常的頻率發(fā)送心跳,就被認為是活躍的,說明它還在讀取分區(qū)里的消息。
消費者會在輪詢消息或提交已讀取偏移量時發(fā)送心跳。如果消費者停止發(fā)送心跳的時間足夠長,會話就會過期,群組協(xié)調(diào)器就會認為它已經(jīng)死亡,就會觸發(fā)一次再均衡。
3. 消息輪詢
消息輪詢是消費者API的核心,通過一個簡單的輪詢向服務器請求數(shù)據(jù)。一旦消費者訂閱了主題,輪詢就會處理所有的細節(jié),包括群組協(xié)調(diào)、分區(qū)再均衡、發(fā)送心跳和獲取數(shù)據(jù)。
4. 提交和偏移量
poll()方法總是返回由生產(chǎn)者寫入到Kafka但還沒有被消費者讀取過的記錄(偏移量)。更新分區(qū)當前位置(偏移量)的操作被稱為提交。
消費者向_consumer_offset的特殊主體發(fā)送消息,消息是包含每個分區(qū)的偏移量。如果消費者發(fā)生崩潰或者有新的消費者加入群組,就會觸發(fā)再均衡,完成再均衡之后,每個消費者可能分配到新的分區(qū)。此時消費者需要讀取每個分區(qū)最后一次提交的偏移量,然后從偏移量指定的位置繼續(xù)處理。
提交的方式:
- 自動提交
如果enable.auto.commit設置為true,每過auto.commit.internal.ms的時間,消費者會自動把從poll()方法接收到的最大偏移量提交上去。消費者每次在進行輪詢時會檢查是否該提交偏移量了,如果是則提交上一次輪詢獲取到的偏移量 - 提交當前偏移量
把enable.auto.commit設為false,讓消費者決定何時提交偏移量。使用commitSync()會提交由poll()獲取的最新偏移量,提交成功后馬上返回,若失敗則拋出異常。手動提交再broker對提交請求作出回應之前,應用程序會一直阻塞 - 異步提交
在成功提交或碰到無法恢復的錯誤之前,commitSync()會一直重試,但是commitAsync不會,之所以不會是因為它收到服務器響應的時候,可能有一個更大的偏移量已經(jīng)提交成功 - 同步和異步組合提交
- 提交指定的偏移量
每個分區(qū)都一個有序、不可變的記錄序列,新的消息會不斷追加到提交日志(commit log)。分區(qū)中的每條消息都會按照時間順序分配到一個單調(diào)遞增的順序編號,叫做偏移量(offset),這個偏移量可以唯一確定當前分區(qū)的任意一條消息
5. 再均衡監(jiān)聽器
ConsumerRebalanceListener,用于監(jiān)聽再均衡事件并處理
6. 從指定偏移量處開始處理記錄
- 從分區(qū)的起始位置開始讀取消息
seekToBeginning(Collection<TopicPartition> tp) - 從分區(qū)的末尾開始讀取消息
seekToEnd(Collection<TopicPartition> tp)
7. 退出
如果確定要退出循環(huán),需要通過另一個線程調(diào)用Consumer#wakeUp()方法;如果循環(huán)運行在主線程里,可以在Runtime#addShutdownHook(Thread)里調(diào)用該方法。Consumer#wakeUp()是消費者唯一一個可以從其他線程里安全調(diào)用的方法,該方法被調(diào)用可以退出poll(),并拋出WakeupException異常,如果線程沒有等待輪詢,那么異常將在下一次調(diào)用poll()時拋出
在退出線程之前有必要調(diào)用Consumer#close(),該方法會提交任何沒有提交的內(nèi)容,并向群組協(xié)調(diào)器發(fā)送消息告知其自己要離開群組,接下來就會觸發(fā)再均衡,而不需要等待會話超時
8. 序列化與反序列化
生產(chǎn)者要用序列化器把對象轉(zhuǎn)換成字節(jié)數(shù)組再發(fā)送給Kafka,消費者需要用反序列化器把從Kafka接收到的字節(jié)數(shù)組轉(zhuǎn)換成Java對象
四、深入Kafka
1. 集群成員關系
Kafka使用Zookeeper來維護集群成員的信息。每個broker都有一個唯一標識符,可以在配置文件指定,也可以自動生成。不能啟動另一個存在相同ID的broker
在broker啟動的時候,它通過創(chuàng)建臨時節(jié)點把自己的ID注冊到Zookeeper。Kafka組件訂閱Zookeeper的/broker/ids路徑(broker在Zookeeper上的注冊路徑),當有broker加入或退出集群時,這些組件就會被通知。
當broker停機、出現(xiàn)網(wǎng)絡分區(qū)或長時間垃圾回收停頓時,broker會從Zookeeper上斷開連接,此時broker在啟動時創(chuàng)建的臨時節(jié)點會自動從Zookeeper上被移除。監(jiān)聽borker列表的Kafka組件會被告知該broker已被移除
2. 控制器
控制器的產(chǎn)生
控制器是一個broker,除了具有普通broker的功能之外,還負責分區(qū)首領的選舉。集群里第一個啟動的broker通過在Zookeeper里創(chuàng)建一個路徑為/controller的臨時節(jié)點讓自己成為控制器。其他broker在啟動的時候也會嘗試創(chuàng)建這個節(jié)點并失敗,并在控制器節(jié)點上創(chuàng)建Zookeeper Watch對象用來接收控制器變更通知
如果控制器被關閉或者與Zookeeper斷開連接,/controller節(jié)點會被刪除。集群中的其他broker通過Watch對象得到控制器節(jié)點斷開的通知,并嘗試讓自己成為新的控制器,非控制器broker重復上述過程
每個新選出的控制器通過Zookeeper的條件遞增操作獲得一個全新的值更大的controller epoch,其他broker在知道當前controller epoch之后,會忽略含有舊epoch的消息
分區(qū)首領的產(chǎn)生
當控制器發(fā)現(xiàn)broker離開集群(觀察相關Zookeeper路徑),它就知道,那些首領在這個broker上的分區(qū)需要一個新的首領??刂破鞅闅v這些分區(qū),并確定誰應該成為新首領(分區(qū)副本列表的下一個副本),然后向所有包含新首領或現(xiàn)有跟隨者的broker發(fā)送請求,該請求消息包含了誰是新首領以及誰是分區(qū)跟隨者的信息。隨后,新首領愛是處理來自生產(chǎn)者和消費者的請求,而跟隨者開始從首領那里復制消息
3. 復制
復制功能是Kafka架構的核心,因為它可以在個別節(jié)點失效時仍能保證Kafka的可用性和持久性
Kafka使用主題來組織數(shù)據(jù),每個主題被分為若干個內(nèi)容不同的分區(qū),每個分區(qū)有多個內(nèi)容相同的副本
副本有兩種類型:
- 首領副本
每個分區(qū)都有一個首領副本,所有的生產(chǎn)者的寫請求和消費者的讀請求都會在首領副本上操作。首領的另一個任務是了解哪個跟隨者副本是跟自己保持一致的。 - 跟隨者副本
首領以外的副本都是跟隨者副本。跟隨者副本不處理來自客戶端的請求,唯一的任務就是從首領復制消息,保持與首領一致的狀態(tài)。持續(xù)請求得到的最新消息副本被稱為同步的副本(ISR)。在首領發(fā)生失效時,只有同步副本才有可能成為新首領
如果跟隨者在指定時間內(nèi)沒有請求任何消息,或者雖然在請求消息,但是沒有請求最新的消息,那么它就不是同步的。如果一個副本無法與首領保持一致,在首領發(fā)生失效時,它不能成為新首領
4. 處理請求
Kafka broker處理請求的過程如圖

- Acceptor線程
負責監(jiān)聽端口并創(chuàng)建連接,并將連接交給Processor線程處理 - Processor線程
負責從客戶端獲取請求消息,并放入請求隊列,然后從響應隊列獲取響應消息,把它們發(fā)送給客戶端。線程數(shù)量可以配置 - IO線程
負責處理請求,并產(chǎn)生響應
主要請求類型
- 生產(chǎn)請求
- 消費請求
- 元數(shù)據(jù)請求
- 其他類型
生產(chǎn)請求和獲取請求都必須發(fā)送給分區(qū)的首領副本。如果broker收到一個指定分區(qū)的請求,而該分區(qū)的首領不在此broker,那么broker會響應“非分區(qū)首領”的錯誤。Kafka客戶端負責把生產(chǎn)請求和獲取請求發(fā)送到正確的broker上
元數(shù)據(jù)請求包含了客戶端感興趣的主題列表,服務端的響應信息里指明了這些主題包含的分區(qū)、每個分區(qū)都有哪些副本,以及哪個副本是首領
5. 物理存儲
Kafka的基本存儲單元是分區(qū),在配置Kafka時,管理員指定了一個用于存儲分區(qū)的目錄清單log.dirs。在創(chuàng)建主題時,Kafka首先會決定如何在broker間分配分區(qū),分區(qū)要達到以下目標:
- 在broker間平均地分布分區(qū)副本
- 確保每個分區(qū)的每個副本分布在不同的broker上
- 如果為broker指定了機架信息,那么盡可能地把每個分區(qū)的副本分配到不同機架的broker上
五、可靠數(shù)據(jù)傳遞
1. Kafka的可靠性保證
- 順序保證:
保證分區(qū)消息的順序。如果使用同一個生產(chǎn)者往同一個分區(qū)寫入消息,而且消息B在消息A之后寫入,那么Kafka可以保證消息B的偏移量比消息A的偏移量大,而且消費者會先讀取消息A后讀取消息B - 提交確認:
只有當消息被寫入到分區(qū)的所有同步副本時(但不一定要寫入磁盤),它才被認為是“提交”的。生產(chǎn)者可以選擇接受不同類型的確認:消息被完全提交時的確認、消息寫入首領副本時的確認、消息被發(fā)送到網(wǎng)絡時的確認 - 消息不丟:
只要還有一個副本是活躍的,那么已經(jīng)提交的消息就不會丟失 - 提交可見:
消費者只能讀取已經(jīng)提交的消息
2. 復制
Kafka的主題被分為多個分區(qū),分區(qū)是基本的數(shù)據(jù)塊。分區(qū)存儲在單個磁盤上,Kafka可以保證分區(qū)里的事件是有序的。分區(qū)可以在線,也可以離線(不可用)。
每個分區(qū)可以有多個副本,其中一個是首領。所有的消息都是直接發(fā)送給首領副本,或者直接從首領副本讀取消息。其他分區(qū)只需要與首領副本保持同步,并及時復制最新的消息。當首領副本不可用時,分區(qū)其他任一同步副本將成為新首領。
同步副本需要滿足以下條件:
- 與Zookeeper之間有一個活躍的會話,在過去6s(可配置)內(nèi)向其發(fā)送過心跳
- 過去10s內(nèi)(可配置)從首領那里獲取過消息
- 在過去10s內(nèi)從首領那里獲取過最新的消息
3. broker配置
3.1 復制系數(shù)
如果復制系數(shù)為N,那么在N - 1個broker失效的情況下,仍然能夠從主題讀取數(shù)據(jù)或向主題寫入數(shù)據(jù)。所以,更高的復制洗漱會帶來更高的可用性、可靠性和更少的故障
3.2 不完全的首領選舉
3.3 最少同步副本
4. 在可靠的系統(tǒng)里使用生產(chǎn)者
4.1 發(fā)送確認
-
acks = 0
如果生產(chǎn)者能夠通過網(wǎng)絡把消息發(fā)送出去,就認為消息已成功寫入Kafka -
acks = 1
首領在收到消息并把它寫入到分區(qū)數(shù)據(jù)文件(不一定同步到磁盤上)是會返回確認或錯誤響應 -
acks = all
首領在返回確認或錯誤響應之前,會等待素有同步副本都收到消息
4.2 配置生產(chǎn)者的重試參數(shù)
4.3 額外的錯誤處理
5. 在可靠的系統(tǒng)里使用消費者
5.1 消費者的可靠配置
-
group.id
如果連個消費者具有相同的group.id,并且訂閱了同一個主題,那么每個消費者會分到主題分區(qū)的一個子集。如果你希望消費者可以看到主題的所有消息,那么需要它們設置唯一的group.id -
auto.offset.reset=ealiest | latest
指定了再沒有偏移量可提交時,或者請求的偏移量在broker上不存在時,消費者的行為
earliest:消費者會動分區(qū)的開始位置讀取數(shù)據(jù),不管偏移量是否有效,這樣會導致消費者讀取大量的重復數(shù)據(jù),但可以保證最少的數(shù)據(jù)丟失
latest:消費者會從分區(qū)的末尾開始讀取數(shù)據(jù),這樣可以減少重復處理消息,單很有可能丟失數(shù)據(jù) -
enable.auto.commit
自動提交偏移量,異步處理消息可能導致提交錯誤的偏移量 -
auto.commit.interval.ms
自動提交偏移量的頻率
5.2 顯式提交偏移量
- 總是在處理完事件后再提交偏移量
- 提交頻度是性能和重復處理消息數(shù)量之間的權衡
- 確保對提交的偏移量心里有數(shù)
- 再均衡
- 消費者可能需要重試
- 消費者可能需要維護狀態(tài)
- 長時間處理
- 僅一次傳遞
Q&A
- 推送消息給消費者和消費者拉取消息各自優(yōu)缺點
broker主動地推送消息給下游的消費者,由broker控制數(shù)據(jù)傳輸?shù)乃俾?,但是broker對下游消費者能否及時處理消息不得而知。如果數(shù)據(jù)的消費速率低于生產(chǎn)速率,消費者就會處于符合狀態(tài),那么發(fā)送給消費者的消息就會堆積得越來越多。而且,推送方式頁難以應付不同類型的消費者,因為不同消費者的消費速率不一定都相同,broker需要調(diào)整不同消費者的傳輸速率,并讓每個消費者充分利用系統(tǒng)的資源。這種方式實現(xiàn)起來比較困難。
消費者從broker主動拉取數(shù)據(jù),broker是無狀態(tài)的,它不需要標記哪些消息時被消費者處理過,也不需要保證一條消息只會被一個消費者處理。而且,不同的消費者可以按照自己最大的處理能力來拉取數(shù)據(jù),及時有時候某個消費者的處理速度稍微落后,它也不會影響其他的消費者,并且在這個消費者恢復處理速度后,仍然可以追趕之前落后的數(shù)據(jù)。
再有就是,推送方式比較難保證消費者正常消費消息狀態(tài)一致性,需要保存每條消息的多種狀態(tài);而拉取方式只需要為每個有序分區(qū)記錄一個偏移量,定時將分區(qū)的消費進度保存成檢查點(checkpoint)文件,不需要記錄消息的任何狀態(tài),而且有需要時,消費者可以回退到某個舊的偏移量位置,重新處理數(shù)據(jù)
