自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Kotlin 云端差分緩存技術(shù)

移動(dòng)開發(fā)
本篇文章我們來具體闡述下 BuildInfra 團(tuán)隊(duì)自研的解決方案 - Kotlin 云端差分方案的原理和技術(shù)實(shí)現(xiàn)。

本文由字節(jié)跳動(dòng) Buildinfra 團(tuán)隊(duì)出品。

在我們的工程上線 Monorepo 全源碼后,Kotlin 編譯成了整個(gè)編譯中最耗時(shí)的步驟,全源碼過程中大量的 BuildCache Miss 導(dǎo)致我們的編譯數(shù)據(jù)落后原來多倉(cāng)二進(jìn)制時(shí)代很多,且業(yè)界沒有相關(guān)的解決方案。本篇文章我們來具體闡述下 BuildInfra 團(tuán)隊(duì)自研的解決方案 - Kotlin 云端差分方案的原理和技術(shù)實(shí)現(xiàn)。

一、Monorepo 中的噩夢(mèng)

在 2022-2023 年,我們的頭部業(yè)務(wù)開始慢慢地從原來的多倉(cāng)二進(jìn)制模式,遷移到全新 Monorepo 方案。

在多倉(cāng)二進(jìn)制時(shí)代,由于 Maven 的加持,大部分時(shí)候我們的都不需要直接編譯代碼,而是復(fù)用 Maven 的『緩存』。

在工程進(jìn)入 Monorepo 時(shí)代之后,我們花了大量的精力建設(shè) BuildCache,來試圖抹平 Monorepo 與二進(jìn)制的構(gòu)建速度差異,但遺憾的是,盡管我們已經(jīng)做了很多的努力,但由于 Gradle 脆弱的 BuildCache 設(shè)計(jì),導(dǎo)致很多時(shí)候改動(dòng)一行代碼就會(huì)全局 miss,不得不重新編譯一遍所有的源碼。這很不環(huán)保(

  • 本地編譯:首次編譯/更新代碼/切分支等場(chǎng)景,非常容易觸發(fā)整個(gè)工程的重新編譯,動(dòng)輒 20min+。
  • CI 編譯:從原來多倉(cāng)二進(jìn)制時(shí)代的 25min 劣化到了 40min+

并且,90 分位的構(gòu)建劣化更是飆升到不可控的地步,它直接決定了本地開發(fā)、CI 合碼的體驗(yàn)。

在可預(yù)見的將來,隨著代碼的快速增長(zhǎng)、更多子倉(cāng)的合入,僅靠 BuildCache 已經(jīng)完全無(wú)法支撐大型 Monorepo 工程的開發(fā)。

二、分析

如果想優(yōu)化 Kotlin 的全量編譯,如果從計(jì)算機(jī)通用優(yōu)化角度來看,不外乎以下幾種方案:

  • 更高效的實(shí)現(xiàn)
  • 并發(fā)
  • 緩存

更高效的實(shí)現(xiàn):語(yǔ)言編譯是一個(gè)龐大且復(fù)雜的系統(tǒng),涉及前后端編譯,目前針對(duì)前后端編譯流程進(jìn)行優(yōu)化實(shí)施起來難度較大,是一個(gè)長(zhǎng)期且艱巨的任務(wù)。但它并不和其他優(yōu)化方案有沖突。

并發(fā):如果是相互之間沒有復(fù)雜依賴關(guān)系的多 Module,那么 Gradle 已經(jīng)保證了模塊之間的并行度;只要你的項(xiàng)目依賴足夠 flat,那么就可以充分利用計(jì)算機(jī)并發(fā)資源,在 CI 場(chǎng)景會(huì)獲得非常大的提升,所以從工程角度可以進(jìn)行依賴的優(yōu)化來提升并行度。但這也不是一個(gè)通用的方案,非常依賴于業(yè)務(wù)方的改造。

緩存:Kotlin 目前使用的是 Gradle 的緩存,眾所周知,Gradle 為了保證正確性,有著一套極其嚴(yán)格的 Cache key 生成策略,很多「可能」不影響 kotlin 編譯正確性的變化(比如編譯插件的 classpath)卻會(huì)讓整個(gè) BuildCache 崩盤,全部 Task 全部 Cache Miss。

在 kotlin 中,不僅是 classpath 這些變動(dòng)會(huì)導(dǎo)致 cache miss,kotlin 的 sourceset(即源碼)也會(huì)作為 cache key 的計(jì)算輸入。這意味著,只要代碼里有一個(gè)字符不一樣,就會(huì)導(dǎo)致 cache miss,整個(gè)模塊就需要重新編譯,那么這塊是否有優(yōu)化空間呢?

綜合來看,從緩存角度入手,可以以比較低成本來實(shí)現(xiàn)較大的收益。

為此,我們提出了一套創(chuàng)新性的方案:

通過云端模糊匹配緩存來將全量編譯轉(zhuǎn)化為增量編譯,來減少全量編譯的耗時(shí)。

三、核心原理

我們通過改造 Kotlin Gradle Plugin 入手,當(dāng) Kotlin Task 由于各種原因不能命中 Build Cache 時(shí),就會(huì) fallback 到我們的『Kotlin 差分編譯系統(tǒng)』。通過云端模糊匹配緩存來將全量編譯轉(zhuǎn)化為增量編譯,來將本來動(dòng)輒 10min+ 的全量編譯轉(zhuǎn)化成 10s 內(nèi)的增量編譯。

云端緩存模糊匹配

常規(guī)編譯構(gòu)建緩存方案如 Gradle Build Cache 采用的是 kv 一對(duì)一匹配。

對(duì)于 Kotlin 來說,即通過對(duì)源碼文件、依賴 Jar 包,編譯參數(shù)等信息算出一個(gè)緩存 key 之后,唯一匹配出一個(gè)緩存。當(dāng)匹配到緩存后,緩存包的內(nèi)容就是 Kotlin 的最終產(chǎn)物,然后就可以跳過 Kotlin Task,直接進(jìn)入下一步。

由于精確匹配需要達(dá)成的條件較多,比如只要修改了一個(gè) .kt文件,就無(wú)法匹配到緩存。

因此可以考慮實(shí)現(xiàn)一套模糊匹配的機(jī)制:

  • 先根據(jù)一些必要參數(shù)匹配一組可能符合要求的緩存
  • 然后從這一組緩存中尋找與當(dāng)前場(chǎng)景最接近的緩存包來進(jìn)行使用

全量轉(zhuǎn)增量

最接近的緩存包,也只是最接近的,是不能直接拿來用的。

那我們是不是可以將這個(gè)緩存包對(duì)應(yīng)的源碼與當(dāng)前源碼做 diff,然后將全量轉(zhuǎn)增量呢?

比如,某個(gè)模塊有 A.kt,B.kt,我們基于某個(gè) Commit 打出來了一個(gè)云端緩存包。

有一天,我們新增了一個(gè) C.kt,改動(dòng)了 B.kt,這時(shí)候,我們找到之前打出來的緩存包,對(duì)他們對(duì)應(yīng)的源碼做一次 diff,那么我們可以得到這組變動(dòng):

[
   changed: ["B.kt"],
   added: ["C.kt"]
]

我們只需要將這個(gè) Diff 輸入給 kotlin 編譯器,kotlin 就會(huì)自動(dòng)幫我們完成耗時(shí)較低的增量編譯

基于 #1#2 的兩個(gè)核心原理,我們?cè)O(shè)計(jì)了一套『Kotlin 差分編譯系統(tǒng)』,整體架構(gòu)圖如下,有四個(gè)重要的角色:

  • 客戶端:Hook Kotlin Compiler,負(fù)責(zé)將全量編譯轉(zhuǎn)為增量編譯。
  • 服務(wù)端:基于客戶端的請(qǐng)求匹配最佳緩存。
  • 種子節(jié)點(diǎn):基于 develop 分支,定時(shí)打包,上傳緩存。
  • 分布式緩存:我們內(nèi)部使用的緩存系統(tǒng),主要通過 K-V 的形式來存儲(chǔ)我們的緩存包。

圖片圖片

四、方案詳解

客戶端方案

客戶端的核心邏輯:對(duì) kotlin 編譯流程進(jìn)行 hook,構(gòu)造/加載緩存包,將全量編譯轉(zhuǎn)換為增量編譯,轉(zhuǎn)換的關(guān)鍵在于增量編譯與全量編譯的差異部分:

  1. 增量編譯環(huán)境中有上次編譯好的編譯產(chǎn)物、增量追蹤文件(符號(hào)引用關(guān)系,abi 信息等)。
  2. 增量編譯相比于上次編譯有哪些輸入文件產(chǎn)生了修改的文件 diff 信息。

圖片圖片

緩存包內(nèi)容

為了能夠在全量編譯時(shí)順利還原出一個(gè)正確的增量編譯現(xiàn)場(chǎng),要求加載的緩存包中應(yīng)該包含的內(nèi)容有:

  1. 編譯產(chǎn)物 (減少不必要的源文件重新編譯耗時(shí))
  2. metadata 包括 kt 、java、layout 文件等 源文件信息,包括這些文件在項(xiàng)目中的相對(duì)路徑以及 hash 值,在還原編譯現(xiàn)場(chǎng)時(shí),根據(jù)這些內(nèi)容還原為源文件的 diff 信息 ( add, remove ,modify)
  3. 編譯器信息:編譯器參數(shù),編譯器 JAR 包的 hash 信息等。
  4. 增量中間產(chǎn)物:涉及到一系列追蹤文件,這些文件包含了kotlin 編譯器在增量編譯時(shí) 用來計(jì)算 dirty file (需要重新編譯的源文件)的關(guān)鍵信息,包括符號(hào)引用關(guān)系、abi 信息、源文件與編譯產(chǎn)物的對(duì)應(yīng)關(guān)系等。

圖片圖片

緩存匹配

如前文中的『核心原理』章節(jié)所描述,我們采取以下方案來進(jìn)行緩存的匹配。

  • 先根據(jù)一些精準(zhǔn)參數(shù)匹配一組可能符合要求的緩存
  • 然后從這一組緩存中尋找與當(dāng)前場(chǎng)景最接近的緩存包來進(jìn)行使用

其中以下參數(shù)會(huì)參與精確參數(shù)(unique_hash)的計(jì)算

  • Kotlin 版本
  • Kotlin Compiler Plugin (KCP)
  • Kotlin 編譯器參數(shù)

我們通過將這些參數(shù)組合到一起組成一個(gè) hash 值:unique_hash。

在之后的流程中,會(huì)使用 unique_hash 來篩選出一組緩存。我們?cè)偻ㄟ^服務(wù)端的策略來篩選出最佳的緩存。

圖片圖片

編譯流程 hook

拿到了緩存包后,我們就可以進(jìn)行緩存包的復(fù)用了。但是我們?cè)谏弦粋€(gè)流程中匹配到的僅僅是最優(yōu)緩存,但是這個(gè)緩存和我們當(dāng)前的包還存在 diff,比如代碼、模塊依賴信息 等的差異。

我們需要計(jì)算出拉到的緩存包和當(dāng)前編譯的 diff,然后將 diff 傳給 kotlin 編譯器,并將全量編譯轉(zhuǎn)成增量編譯。

為了能夠?qū)崿F(xiàn)增量編譯的轉(zhuǎn)換,需要在編譯流程的三個(gè)階段進(jìn)行 hook

  1. 編譯前執(zhí)行前解析模塊依賴信息。
  2. 在提交編譯參數(shù)到編譯器執(zhí)行前,匹配、下載、校驗(yàn)、加載緩存包,將全量編譯參數(shù)轉(zhuǎn)換為增量編譯參數(shù)。
  3. 編譯完成后構(gòu)造并上傳緩存包。

圖片圖片

其中,最核心的 Hook 點(diǎn)在于:將全量編譯轉(zhuǎn)化成增量編譯。

internal fun CallCompilerAsyncOpt.callCompilerAsyncOpt(
    ....
) {
    val icEnv = IncrementalCompilationEnvironment(
            changedFiles,
            classpathChanges,
            workingDir ,
            ...
        )
    val hookedIcEnv = hook(icEnv)

    val environment = GradleCompilerEnvironment(
        incrementalCompilationEnvironment = hookedIcEnv,
        outputFiles,...
    )
    optRunJvmCompilerAsync(..., source, args, environment , ... )
}

我們會(huì) Hook KotlinCompile 中的 callCompilerAsync 方法,將原來的全量 Env 轉(zhuǎn)換成增量 Env。增量的 InputChanges 來源于我們的緩存包和編譯的 diff (源碼、依賴的變更等)。

而這個(gè)關(guān)鍵的全量轉(zhuǎn)增量 Hook 步驟為:

  1. 匹配下載緩存:為了能夠匹配到最接近的緩存包,實(shí)現(xiàn)最佳增量編譯效率,需要對(duì)這些參數(shù)進(jìn)行比較:
  1. 根據(jù) metadata 中 java/kotlin/layout 文件的 hash 比較源碼文件的 diff (其中包括 layout xml 是因?yàn)?KAE 會(huì)根據(jù) layout 進(jìn)行 kotlin 代碼插樁)
  2. 利用 kotlin 1.7+ 的 classpath-snapshot.bin 計(jì)算依賴模塊依賴信息的 abi diff
  1. 校驗(yàn)加載緩存: 基于緩存包中的 metadata ,結(jié)合編譯產(chǎn)物、編譯中間產(chǎn)物等,將對(duì)應(yīng)信息加載到對(duì)應(yīng)位置。
  2. 構(gòu)造增量編譯參數(shù): 將加載的緩存包中的相對(duì)路徑轉(zhuǎn)換為絕對(duì)路徑,用絕對(duì)路徑的 diff 信息等構(gòu)造出可用的增量 Env , 并提交給 Kotlin Compiler

預(yù)期之外的問題

理論上來講,如果 kotlin 編譯器是能保證增量編譯是完全不出錯(cuò)的,那么我們的方案肯定不會(huì)引入穩(wěn)定性的問題。

但是這樣的期待或許過高。我們的方案是基于 Kotlin 1.7.21 進(jìn)行開發(fā)的,kotlin 在 1.7+ 引入了新的 kotlin 增量編譯方案,還在一個(gè)相對(duì)不穩(wěn)定的階段。實(shí)際上,我們也在上線前后遇到了各種奇怪的問題。經(jīng)過長(zhǎng)時(shí)間的 debug 和翻閱源碼后得到了解決,目前在抖音項(xiàng)目上已經(jīng)趨于穩(wěn)定。

跨端緩存復(fù)用問題

問題表現(xiàn)為:OSX 復(fù)用 ci 種子節(jié)點(diǎn)打包的緩存時(shí),對(duì)于部分刪除的源代碼文件,并沒有將其對(duì)應(yīng)的編譯產(chǎn)物刪除

通過一系列的排查發(fā)現(xiàn),該問題是 Kotlin Compiler 自身的 bug ,且在高版本進(jìn)行了修復(fù),bug 來源于 kotlin 的增量編譯中間產(chǎn)物中,用于追蹤源碼文件與編譯產(chǎn)物關(guān)系的信息異常,在涉及到大小寫敏感不一致的系統(tǒng)時(shí),無(wú)法正常刪除編譯產(chǎn)物。

解決方案: 對(duì)高版本的修復(fù) commit 進(jìn)行了 cherry-pick

kotlin 修復(fù) https://github.com/JetBrains/kotlin/commit/be71d8841ebc22c79bb6b4bc6f3ad93c147ba9c0

大小寫敏感問題

由于 OSX 不是一個(gè)大小寫敏感的操作系統(tǒng),這意味著,在一個(gè)文件夾中,不能同時(shí)存在去掉大小寫后字符一樣的兩個(gè)文件。比如:

圖片圖片

但是這個(gè)行為在 Linux 系統(tǒng)上是允許的。

我們的遇到的問題是,項(xiàng)目中不同文件夾中有兩個(gè)包名叫 com.xxx.legoImp ,一個(gè)叫com.xxx.legoimp 。這兩個(gè)包名會(huì)在 Linux 種子節(jié)點(diǎn)上進(jìn)行 Jar 包壓縮,在 mac 上進(jìn)行解壓 ,由于 Linux 不是 Case Sensitive 的系統(tǒng),所以 jar 包中,他倆會(huì)同時(shí)存在,但是在 mac 上解壓時(shí),mac 會(huì)自動(dòng)合并了這兩個(gè)包,并且轉(zhuǎn)換成小寫。這樣就會(huì)在運(yùn)行時(shí)崩潰,找不到這個(gè)包名。

解決方案:我們提供了一個(gè)類似的包名大小寫檢測(cè),幫助業(yè)務(wù)提前暴露問題,并進(jìn)行代碼的整改。

方案性能極致優(yōu)化

加速緩存解壓

在最初的緩存包壓縮中,采用的是 Java ZipFile 中默認(rèn)的壓縮算法,上線后發(fā)現(xiàn)有較多模塊的緩存解壓時(shí)間較長(zhǎng),后續(xù)參考了Gradle build-cache 方案,將壓縮算法修改為 lz4,大大降低了解壓縮的時(shí)間。

降低緩存淘汰率

在本方案中,緩存遠(yuǎn)端存儲(chǔ)在我們的分布式緩存服務(wù)中,由于分布式緩存空間有限(2TB),超過緩存空間時(shí)會(huì)根據(jù) LRU 淘汰最近未使用的緩存,這樣就會(huì)導(dǎo)致緩存命中率下降,而由于在抖音項(xiàng)目中,生產(chǎn)緩存的種子節(jié)點(diǎn)會(huì)在每個(gè) MR 合入后進(jìn)行緩存打包,每次全量打包緩存時(shí),緩存包約1GB+ , 為了減少不必要的緩存上傳,采取了兩個(gè)方案:

  1. 與 build-cache 進(jìn)行關(guān)聯(lián),如果種子節(jié)點(diǎn)打包時(shí)命中 build-cache ,將 kotlin 緩存與 build-cache 進(jìn)行關(guān)聯(lián),而不是重復(fù)上 kotlin 緩存
  2. 計(jì)算緩存包內(nèi)容 hash ,如果存在相同的緩存包,將他們關(guān)聯(lián)到一起,而不是重復(fù)上傳緩存。

服務(wù)端策略

區(qū)別于傳統(tǒng)的 Build Cache 服務(wù)端的簡(jiǎn)單 K-V 存儲(chǔ),我們的服務(wù)端需要幫助客戶端去存儲(chǔ)產(chǎn)物、匹配產(chǎn)物、選擇最佳產(chǎn)物??蛻舳撕头?wù)端的通信時(shí)序圖如下:

圖片圖片

其中,服務(wù)端最核心的邏輯在于匹配最佳產(chǎn)物。

每一個(gè) unique hash 都會(huì)對(duì)應(yīng)一組產(chǎn)物,我們要從這一組產(chǎn)物里找到一個(gè)最優(yōu)解。

最優(yōu)解的定義是:這個(gè)產(chǎn)物離我們當(dāng)前的編譯較為接近。我們?yōu)檫@個(gè)『接近』,定義了一個(gè)記分制度。

data class Diff(val added: List<String>, 
                val removed: List<String>,
                val modified: List<String>)
fun diffScore(): Int {
    return this.diff.removed.size * 3 +
            this.diff.modified.size * 2 +
            this.diff.added.size
}

每個(gè)產(chǎn)物和當(dāng)前編譯都會(huì)產(chǎn)生一個(gè) diffdiff 里有 added、removedmodified。

我們定義一個(gè)函數(shù) diffScoreremove 記三分,modified 記兩分,added 記一分。

之所以按照這樣的系數(shù),是因?yàn)?removed 大概率會(huì)引起很多底層依賴的重新編譯,所以我們需要盡可能地減少 removed 的代碼,added 的代碼一般不會(huì)引起其它代碼重新編譯,所以我們可以記為一分。modified 介于兩者之間,記兩分。

那么,我們的核心的邏輯就變成了:從一組緩存里,找到 diffScore 最少的產(chǎn)物。

但是因?yàn)槲覀兊?unique hash 對(duì)應(yīng)的 hash 生成并不復(fù)雜,一般來說,除非是升級(jí) kotlin 版本,其它情況這個(gè) unique hash 基本不會(huì)變,所以這個(gè)緩存的量可能會(huì)很大,我們不可能實(shí)現(xiàn)找到所有的緩存,并且進(jìn)行 diffScore 的比較。我們需要篩選出一定范圍內(nèi)的緩存。

首先,我們的所有的產(chǎn)物生成都是在主干分支上。

那么意味著,對(duì)于 feature 分支,與其代碼最接近的 commit 應(yīng)該是圖中的 base commit

圖片圖片

因?yàn)槌杀镜脑?,種子任務(wù)可能并不會(huì)在主干分支的每個(gè)節(jié)點(diǎn)都打緩存包,可能是定時(shí)打包。那么意味著不是所有的 merge commit 都有緩存包。

所以,我們找到這個(gè) commit 的前后一天時(shí)間內(nèi)的 commit。并和表中的所有產(chǎn)物進(jìn)行取交集。

圖片圖片

最后得到所有黃色的點(diǎn)。(黃色的點(diǎn)是打了緩存包的 merge 節(jié)點(diǎn))

我們將所有黃色的點(diǎn)對(duì)應(yīng)的 MetaData 與當(dāng)前 commit 的 MetaData 進(jìn)行 diff。

取一個(gè) diffScore 最少的節(jié)點(diǎn),作為最終的產(chǎn)物。

val bestArtifact = artifacts.minOf {
    it.diffScore()
}

這樣我們就可以認(rèn)為它是和當(dāng)前 commit 最接近的 commit。對(duì)應(yīng)的產(chǎn)物對(duì)于客戶端來說,轉(zhuǎn)增量編譯的風(fēng)險(xiǎn)最小、速度最快。

五、上線收益

目前項(xiàng)目上線了大部分的字節(jié) Monorepo 項(xiàng)目,穩(wěn)定運(yùn)行了半年有余。期間也修復(fù)了 kotlin 1.7 上的若干官方增量編譯相關(guān)的 bug,最后取得了較為理想的收益。 

抖音從二進(jìn)制演進(jìn)到 Mopnorepo 后,Kotlin 部分的編譯時(shí)間劣化超過 10m+,我們通過本文的方案,將劣化的部分基本抹平,90 分位下降 60%,大幅提升了開發(fā)的體驗(yàn)和 CI 合碼效率。

六、總結(jié)

本文主要闡述了 Kotlin 云端差分方案的技術(shù)原理。

實(shí)現(xiàn)超大型工程 Monorepo 全源碼的研發(fā)模式,僅靠 BuildCache 是無(wú)法實(shí)現(xiàn)的,Kotlin 云端差分方案對(duì)于全源碼起著不可或缺的作用。

通過模糊匹配緩存的方式將全量編譯模擬成增量編譯的思想,可能在其他端上比較容易實(shí)現(xiàn)或者就是標(biāo)準(zhǔn)方案,但目前 Android CI 構(gòu)建領(lǐng)域均采用 clean 編譯,無(wú)法復(fù)用構(gòu)建中間產(chǎn)物。該方案的出現(xiàn)也有望實(shí)現(xiàn)思想的復(fù)用,運(yùn)用到所有『類似』的任務(wù)中,比如 Java Compile、Transform、Dex 等,只要它本身是支持增量、編譯冪等特性的,都可以復(fù)用這套方案,從而進(jìn)一步提升 Android 的構(gòu)建效率。

在研究云端差分方案期間,我們積累了大量的 Kotlin 編譯器相關(guān)的知識(shí),為我們平時(shí)排查 kotlin 疑難問題提供了非常多的技術(shù)儲(chǔ)備,也發(fā)現(xiàn)了很多 kotlin 官方的 bug 和設(shè)計(jì)上的缺陷,我們也會(huì)在將來修復(fù)后回饋 kotlin 社區(qū)。

歡迎對(duì)編譯構(gòu)建、Kotlin相關(guān)技術(shù)感興趣的小伙伴找我們進(jìn)行技術(shù)交流與討論!

責(zé)任編輯:龐桂玉 來源: 字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)
相關(guān)推薦

2022-04-06 15:58:25

火山引擎差分隱私LDPDC

2023-09-03 19:13:29

AndroidKotlin

2015-04-27 14:42:24

技術(shù)架構(gòu)服務(wù)器性能

2012-04-02 16:35:49

網(wǎng)絡(luò)緩存

2012-12-26 09:32:55

智能手機(jī)移動(dòng)傳感平臺(tái)云端分載GPS

2012-03-13 08:56:39

2023-11-11 19:43:12

緩存數(shù)據(jù)庫(kù)

2016-02-01 17:17:38

AWS技術(shù)峰會(huì)AWS Summit2

2016-01-04 11:04:47

無(wú)線技術(shù)傳感技術(shù)物聯(lián)網(wǎng)

2015-05-13 13:13:34

2018-08-07 10:44:50

緩存技術(shù)瀏覽器

2011-07-11 10:00:34

PHP緩存技術(shù)

2009-07-29 15:38:01

2010-06-01 15:35:32

2021-01-13 12:10:09

物聯(lián)網(wǎng)隱私網(wǎng)絡(luò)安全

2011-12-28 10:44:02

PowerVM虛擬化

2015-09-09 10:20:00

php緩存技術(shù)

2010-07-21 09:38:15

PHP緩存技術(shù)

2024-01-19 09:21:35

攜程開源

2016-12-01 15:03:36

緩存技術(shù)客戶端
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)