用Go重构WEB请求分析跟踪服务
在Skroutz,我們嚴(yán)重依賴網(wǎng)頁(yè)分析來(lái)進(jìn)行關(guān)鍵業(yè)務(wù)和技術(shù)決策。 從網(wǎng)頁(yè)瀏覽收集的數(shù)據(jù)可以用于計(jì)算商店產(chǎn)品轉(zhuǎn)換率,提取商業(yè)智能,制定個(gè)性化建議和預(yù)測(cè)的原材料。
隨著訪問(wèn)流量在過(guò)去幾年中穩(wěn)步增長(zhǎng),我們面臨一些挑戰(zhàn),促使我們重新設(shè)計(jì)我們業(yè)務(wù)的關(guān)鍵部分:網(wǎng)站數(shù)據(jù)分析跟蹤系統(tǒng)
網(wǎng)站數(shù)據(jù)分析跟蹤系統(tǒng)1.0
從網(wǎng)站上采集數(shù)據(jù)信息通常涉及使用web beacons(信標(biāo)),這是一個(gè)花哨的名字,就像為無(wú)形的圖像注入html標(biāo)簽。
一個(gè)類似beacon的例子如下:
<img width="1" height="1" src="https://www.scrooge.co.uk/track?foo=bar"></img>將這樣的片段添加到我們要跟蹤的頁(yè)面中, 之后每次Web瀏覽器都會(huì)訪問(wèn)這些頁(yè)面,它通過(guò)向www.scrooge.co.uk/track?foo=bar發(fā)出請(qǐng)求來(lái)獲取圖像,該請(qǐng)求將由我們的服務(wù)器處理,服務(wù)器將從查詢字符串中獲取跟蹤數(shù)據(jù)。
在這種情況下,查詢字符串是foo = bar, 這些是原始數(shù)據(jù),在轉(zhuǎn)換為更方便處理的格式后,我們的應(yīng)用程序?qū)?shù)據(jù)存儲(chǔ)起來(lái)供以后使用。 這實(shí)際上是Google Analytics(分析)的工作原理,以及Skroutz的網(wǎng)頁(yè)分析工作。
這個(gè)博客的重點(diǎn)是跟蹤請(qǐng)求到達(dá)我們的服務(wù)器后,直到數(shù)據(jù)被持續(xù)進(jìn)行進(jìn)一步分析。
最初
傳統(tǒng)的實(shí)現(xiàn)方式相當(dāng)簡(jiǎn)單,主要涉及我們的Rails應(yīng)用程序。
跟蹤請(qǐng)求由我們的Rails整體運(yùn)行在Unicorn上。 這是一個(gè)適用于所有常規(guī)用戶流量的應(yīng)用程序(例如https://www.scrooge.co.uk/c/165/mobile_phones.html)。
我們目前在三個(gè)國(guó)家運(yùn)營(yíng),每個(gè)國(guó)家都有自己部署的應(yīng)用實(shí)例。 希臘的www.skroutz.gr,土耳其的www.alve.com和英國(guó)的www.scrooge.co.uk。 每個(gè)實(shí)例都有自己的應(yīng)用服務(wù)器,數(shù)據(jù)庫(kù)和其他基礎(chǔ)架構(gòu)。
傳入的查詢參數(shù)將轉(zhuǎn)換為JSON對(duì)象。 例如?foo = bar被轉(zhuǎn)換為{“foo”:“bar”}。
然后使用阻塞調(diào)用將JSON對(duì)象保存在磁盤上的文件和Kafka中。(日志是原始存儲(chǔ),直到Kafka被添加到混合。
這種冗余是一個(gè)中間的情況,直到我們完全過(guò)渡到Kafka。直到這兩個(gè)存儲(chǔ)是同樣重要的,因?yàn)橛袘?yīng)用程序依賴于兩者之一。Memcached被用作獨(dú)角獸工作者之間的共享存儲(chǔ),用于執(zhí)行正確和錯(cuò)誤檢查。)
以上所有這些都在HTTP請(qǐng)求生命周期內(nèi)進(jìn)行:瀏覽器請(qǐng)求 "/trace?foo = bar",數(shù)據(jù)保存到日志文件和Kafka之后,響應(yīng)將發(fā)送回客戶端。
流程如下圖所示:
舊結(jié)構(gòu)
雖然這個(gè)解決方案為我們服務(wù)好幾年,但是入境流量不斷增長(zhǎng),我們開(kāi)始擔(dān)心,因?yàn)槲覀冎烙幸恍撛诘膯?wèn)題。
動(dòng)機(jī):為什么困擾?
最關(guān)鍵的問(wèn)題是跟蹤請(qǐng)求可能會(huì)導(dǎo)致應(yīng)用程序掛掉不能為用戶提供正常服務(wù)。 可能導(dǎo)致以下后果:
1.每個(gè)頁(yè)面視圖都會(huì)產(chǎn)生后續(xù)跟蹤請(qǐng)求。
2.跟蹤請(qǐng)求由提供常規(guī)用戶流量的同一應(yīng)用程序和服務(wù)器(例如,頁(yè)面瀏覽,API調(diào)用)提供。
3.使用阻止調(diào)用,在跟蹤請(qǐng)求生命周期內(nèi)執(zhí)行將數(shù)據(jù)保存到Kafka和日志文件。
4.每個(gè)獨(dú)角獸工作者一次可以為一個(gè)客戶服務(wù)。
這樣糟糕的是,其中一個(gè)存儲(chǔ)(即Kafka,NFS)的故障可能導(dǎo)致跟蹤請(qǐng)求,迅速占據(jù)所有可用的Unicorn服務(wù)器,沒(méi)有服務(wù)器為其他客戶提供服務(wù),導(dǎo)致停機(jī)。
這是不好的, 由于我們的網(wǎng)站數(shù)據(jù)分析跟蹤出現(xiàn)問(wèn)題,我們的用戶體驗(yàn)可能會(huì)受損。
在軟件架構(gòu)方面,主要應(yīng)用程序與可追溯性服務(wù)的耦合是不必要的。 雖然應(yīng)用程序通常每天部署多次,但跟蹤邏輯在幾年內(nèi)只會(huì)改變幾次。 這導(dǎo)致開(kāi)發(fā)人員不愿意改變可能以某種方式影響跟蹤路徑的內(nèi)容(例如升級(jí)Kafka驅(qū)動(dòng)程序)。
此外,請(qǐng)求沒(méi)有必要通過(guò)整個(gè)Rails ,導(dǎo)致應(yīng)用服務(wù)器中的資源和容量浪費(fèi)。
除了這些問(wèn)題之外,還有一個(gè)事實(shí),就是沒(méi)有強(qiáng)大的Ruby Kafka驅(qū)動(dòng)程序存在。 我們使用了一些,但遇到了許多關(guān)鍵錯(cuò)誤,而我們修復(fù)了其中一些錯(cuò)誤,其他則需要進(jìn)行重大改寫。
顯然是時(shí)候進(jìn)行大修了。 我們決定退后一步,重新考慮問(wèn)題,并提出更好的長(zhǎng)期解決方案。
構(gòu)建新系統(tǒng)
出現(xiàn)的第一個(gè)問(wèn)題是“如果我們將跟蹤邏輯提取到單獨(dú)的服務(wù)怎么辦?
假設(shè)我們這樣做,跟蹤路徑的故障不會(huì)傷害用戶體驗(yàn),因?yàn)楦櫡?wù)的停機(jī)不會(huì)導(dǎo)致主應(yīng)用程序的停機(jī)。
這樣做是很有道理的,用于收集網(wǎng)站分析的代碼不需要與主應(yīng)用程序的代碼相結(jié)合。 跟蹤部分可以被視為一個(gè)獨(dú)立的系統(tǒng),它接收HTTP請(qǐng)求作為輸入,并產(chǎn)生JSON對(duì)象作為輸出。
此外,與主應(yīng)用程序分離的服務(wù)意味著它可以是多租戶:單個(gè)實(shí)例可以為skroutz.gr,alve.com和scrooge.co.uk提供所有流量。
新系統(tǒng)應(yīng)該是可靠的,可維護(hù)的,能夠有效地處理數(shù)千個(gè)客戶,并擴(kuò)展我們的流量。 考慮到這些要求,我們可以對(duì)我們應(yīng)該使用哪種工具做出明智的決定。
選擇正確的工具
作為Ruby的重用戶,這當(dāng)然是我們考慮的第一個(gè)選擇。 然而,我們知道,在MRI Ruby之上編寫可擴(kuò)展,高度并發(fā)的系統(tǒng)將是一個(gè)幾乎不可能的任務(wù)。 即使我們這樣做,結(jié)果也不一定優(yōu)化,因?yàn)檫\(yùn)行時(shí)沒(méi)有內(nèi)置的并發(fā)支持(加上有一個(gè)全局的VM鎖),垃圾收集器將成為主要的障礙。
下一個(gè)選項(xiàng)是Go。 我們一直喜歡這種語(yǔ)言,它的哲學(xué)對(duì)我們來(lái)說(shuō)是很有意義的。 由于以下原因,它似乎是一個(gè)理想的候選人:
·內(nèi)置并發(fā)支持。
·簡(jiǎn)單:任何開(kāi)發(fā)人員都可以快速接收該項(xiàng)目。 代碼庫(kù)將更容易維護(hù)。
·固體標(biāo)準(zhǔn)庫(kù):我們可以使用較少的外部依賴性,開(kāi)發(fā)更可靠和可維護(hù)的系統(tǒng)。
·優(yōu)秀的工具:建立這樣的生產(chǎn)系統(tǒng)時(shí),像數(shù)據(jù)競(jìng)賽檢測(cè)器,執(zhí)行追蹤器,pprof,go vet和gofmt這樣的工具是巨大的優(yōu)勢(shì)。
·文檔:當(dāng)有良好的文檔存在時(shí),該語(yǔ)言更容易使用。
總的來(lái)說(shuō),Go似乎是正確的工具。
scratchd: 新的實(shí)現(xiàn)
名稱scratchd是“scratch daemon”的縮寫,因?yàn)樯鲜鋈罩疚募v史上被稱為“scratch logs”。
新實(shí)現(xiàn)本質(zhì)上是一個(gè)HTTP服務(wù)器和兩個(gè)工作隊(duì)列,一個(gè)用于將數(shù)據(jù)保存到日志文件,另一個(gè)用于將數(shù)據(jù)持久化到Kafka。 我們調(diào)用隊(duì)列"backends",與HTTP服務(wù)器同時(shí)運(yùn)行的協(xié)程,并負(fù)責(zé)持久化數(shù)據(jù)。
在不同渠道傳遞的核心實(shí)體是Line:
// Line corresponds to an incoming tracking request and contains the data // to be persisted. type Line struct {// Flavor specifies the instance to which the request corresponds to// (e.g. scrooge.co.uk).Flavor *Flavor// Values are the query parameters encoded in JSON.Values []byte// Time is the flavor-aware time the request was received.Time time.Time }
backend的定義如下:
每個(gè)傳入的請(qǐng)求都會(huì)產(chǎn)生一個(gè)新的Line值,然后傳遞給backends。 流程如下圖所示:
scratchd內(nèi)部
每個(gè)圓是一個(gè)單獨(dú)的協(xié)程,灰色箭頭表示通過(guò)通道發(fā)送的Line值,而白色箭頭是數(shù)據(jù)持續(xù)到某種存儲(chǔ)。 (為了簡(jiǎn)單起見(jiàn),實(shí)際上有更多的移動(dòng)部件被取出)
HTTP請(qǐng)求生命周期的過(guò)程如下:
1.跟蹤請(qǐng)求進(jìn)入并由HTTP處理程序處理,HTTP處理程序執(zhí)行以下任務(wù):
·請(qǐng)求正確/錯(cuò)誤檢查,如果格式錯(cuò)誤,請(qǐng)?zhí)崆巴顺?br />
·將查詢參數(shù)轉(zhuǎn)換為JSON?
·創(chuàng)建一個(gè)Line值,并通過(guò)緩沖通道將其發(fā)送給backends進(jìn)行持久化
·回應(yīng)客戶?
2.同時(shí),每個(gè)backend同時(shí)運(yùn)行,并且由一個(gè)緊密的循環(huán)(調(diào)度程序)組成,它們從一個(gè)通道接收Line值,并相應(yīng)地保持它們。
文件分派器通過(guò)單獨(dú)的工作程序協(xié)程將數(shù)據(jù)寫入文件。 每個(gè)worker負(fù)責(zé)寫入某個(gè)文件(每天都有一個(gè)文件)。
Kafka backend由一個(gè)worker組成,他們將數(shù)據(jù)保存到Kafka。 由于它由librdkafka支持,它保留了自己的緩沖區(qū)并在引擎蓋下使用了多個(gè)I / O線程,所以在Go空間中不需要進(jìn)一步的并發(fā)。
在高層次上,系統(tǒng)由同時(shí)運(yùn)行的三個(gè)主要組件和通過(guò)通道進(jìn)行通信。
這種方法有很多優(yōu)點(diǎn):
首先,持久性backends是相互分離的。Kafka的失敗不會(huì)對(duì)日志文件產(chǎn)生影響,反之亦然。這樣做的結(jié)果是,由于失敗,日志文件中的數(shù)據(jù)丟失,我們可以使用Kafka重新生成它們。
HTTP路徑完全不受后端任何故障的影響:如果后臺(tái)關(guān)閉,我們會(huì)收到通知,但用戶不會(huì)注意到一件事情。
正在緩沖的通道意味著我們對(duì)任何類型的小問(wèn)題(網(wǎng)絡(luò),Kafka,文件系統(tǒng))都具有更強(qiáng)的彈性,因?yàn)樽鳂I(yè)將被緩沖一段時(shí)間,并最終由相應(yīng)的后端處理。
Kafka驅(qū)動(dòng)的明顯選擇是sarama,是目前最受歡迎的選擇。 然而,由于我們是優(yōu)秀的librdkafka的用戶,我們經(jīng)歷了最強(qiáng)大的Kafka驅(qū)動(dòng)程序?qū)崿F(xiàn)。 利用librdkafka意味著驅(qū)動(dòng)程序通常會(huì)比替代方案更快地獲得錯(cuò)誤修復(fù)和新功能。
Memcached的使用被一個(gè)常駐內(nèi)存中的鍵值存儲(chǔ)器所替代,它在幾行代碼中實(shí)現(xiàn),并支持簡(jiǎn)單的GET / SET操作,TTL過(guò)期,只有字符串作為鍵/值。 雖然有其他緩存實(shí)現(xiàn)可用,但它們提供了比我們需要的更多功能,因此更復(fù)雜。
就配置而言,我們考慮了YAML,TOML和JSON。 由于事實(shí)上標(biāo)準(zhǔn)庫(kù)中有一個(gè)實(shí)現(xiàn),所以我們選擇了更簡(jiǎn)單的JSON。 我們通過(guò)源代碼中的完整文檔,提高代碼的可讀性。
對(duì)于日志記錄,我們使用標(biāo)準(zhǔn)庫(kù)的記錄器,其前綴與每個(gè)組件(http,kafka,file)對(duì)應(yīng)。 輸出由journald收集,然后將其轉(zhuǎn)發(fā)到syslog。 我們可能會(huì)考慮logrus在未來(lái)(Sentry integration?很好),雖然我們還沒(méi)有出售它。
測(cè)試
除了單元測(cè)試,我們主要使用集成測(cè)試:輪詢服務(wù)器,向其發(fā)送用戶請(qǐng)求并驗(yàn)證輸出是否正確。在文件backend的情況下,我們驗(yàn)證在測(cè)試期間生成的日志文件是否正確。同樣,我們使用專用的Kafka集群,通過(guò)消費(fèi)相關(guān)的topics來(lái)驗(yàn)證Kafka后端的輸出。
使用協(xié)程和標(biāo)準(zhǔn)庫(kù)的測(cè)試框架進(jìn)行此操作是相當(dāng)簡(jiǎn)單的:在單獨(dú)的協(xié)程中調(diào)用main(),并從TestMain()發(fā)出客戶端請(qǐng)求。
我們沒(méi)有使用任何外部庫(kù)進(jìn)行測(cè)試,因?yàn)?/span>testing包足夠我們使用。在打印測(cè)試失敗時(shí),我們大量使用reflect.DeepEqual來(lái)比較預(yù)期和實(shí)際結(jié)果。最后,表驅(qū)動(dòng)測(cè)試大大簡(jiǎn)化了實(shí)際的測(cè)試代碼。
為了確保我們沒(méi)有引入任何回歸,我們根據(jù)舊的和新的實(shí)現(xiàn)重現(xiàn)了大量的生產(chǎn)請(qǐng)求,并驗(yàn)證了結(jié)果是一致的。
零停機(jī)部署
部署而不會(huì)失去任何流量是一個(gè)艱巨的要求。 這是先前由Unicorn處理的,并涉及到發(fā)送信號(hào)以控制一些Unicorn進(jìn)程的自定義shell腳本。 該過(guò)程與nginx使用的過(guò)程類似。
我們利用systemd的socket激活, 這樣我們就不必執(zhí)行Unicorn和nginx所做的信號(hào)處理邏輯,而且我們擺脫了shell腳本。 go-systemd包使得這樣做很輕松, 這個(gè)過(guò)程只是一個(gè)綁定和收聽(tīng)由systemd提供的socket,而不是創(chuàng)建一個(gè)新的socket。
我們還利用Go 1.8中添加的優(yōu)雅的服務(wù)器關(guān)機(jī)功能,因?yàn)槲覀儾幌M诓渴鹌陂g強(qiáng)制關(guān)閉使用中的連接。
零停機(jī)重新啟動(dòng)意味著零停機(jī)升級(jí),因?yàn)樯?jí)是更換磁盤二進(jìn)制文件并重新啟動(dòng)systemd服務(wù)的問(wèn)題。
監(jiān)控
除了強(qiáng)制性的Icinga和Munin監(jiān)控,我們使用由Graphite支持的Grafana。 該服務(wù)提供一個(gè)HTTP統(tǒng)計(jì)endpoint,顯示各種指標(biāo),其中一些是:
每個(gè)后端緩沖的作業(yè)數(shù)
每個(gè)后端中的持久性錯(cuò)誤數(shù)
惡意/異常的請(qǐng)求數(shù)
運(yùn)行時(shí)指標(biāo),通過(guò)runtime.MemStats(GC循環(huán)/暫停時(shí)間,內(nèi)存分配等)
該服務(wù)維護(hù)由各種組件(比如backends, HTTP handler)同時(shí)更新的計(jì)數(shù)器的全局映射,因此我們大量使用sync/atomic,這不是理想的,但由于并發(fā)映射將被發(fā)送,所以情況會(huì)更好在Go 1.9的標(biāo)準(zhǔn)庫(kù)中。
在cron中安排的腳本定期收集統(tǒng)計(jì)endpoint公開(kāi)的指標(biāo),并將它們饋送給Graphite。
scratchd運(yùn)行時(shí)指標(biāo)
推出
我們是Debian的重用戶,所以在我們這個(gè)案例中,所以我們做了安裝服務(wù)就像運(yùn)行一樣簡(jiǎn)單:
$ apt-get install scratchd
這樣可以確保所有的依賴關(guān)系得到保護(hù)(即librdkafka),并且還提供默認(rèn)配置文件和相應(yīng)的systemd單元文件。 升級(jí)也很輕松:升級(jí)軟件包并重新啟動(dòng)服務(wù)。
在初步部署過(guò)程中,我們利用了HAproxy被部署在任何backend服務(wù)之前的代理請(qǐng)求。 最初我們只切換了內(nèi)部總部網(wǎng)絡(luò)的流量,在驗(yàn)證一切正常工作后,我們代理了一小部分真正的用戶流量,同時(shí)保留了舊版(Rails應(yīng)用程序)服務(wù)。 我們逐漸增加新服務(wù)的流量,直到?jīng)]有跟蹤請(qǐng)求再次觸發(fā)以前的實(shí)現(xiàn)。 這個(gè)策略幫助我們盡可能減少可能出現(xiàn)的潛在問(wèn)題。
除此之外,沒(méi)有理由使用nginx或Varnish,所以我們擺脫了它們。 新的結(jié)構(gòu)現(xiàn)在減少到以下:
新結(jié)構(gòu)
如果主要實(shí)例出現(xiàn)故障,HAproxy還會(huì)自動(dòng)重定向所有流量。
結(jié)果一目了然
新的解決方案無(wú)縫地解決了我們以前的問(wèn)題,并帶來(lái)了額外的好處:
Web分析跟蹤路徑對(duì)主應(yīng)用程序沒(méi)有任何影響。Kafka或文件系統(tǒng)故障不會(huì)導(dǎo)致瀏覽www.skroutz.gr,www.alve.com或www.scrooge.co.uk的用戶停機(jī)。
持久性backend彼此分離,這意味著Kafka的故障不會(huì)影響日志文件,反之亦然。
彈性:容錯(cuò)性更高。偶然的網(wǎng)絡(luò)延遲或Kafka重新平衡意味著寫入將被緩沖一段時(shí)間,最終將被刷新。這同樣適用于日志文件。
效率:新服務(wù)的一個(gè)實(shí)例部署在一個(gè)虛擬機(jī)上,負(fù)責(zé)處理所有流量(目前來(lái)自三個(gè)國(guó)家),擁有60MB的內(nèi)存占用空間,CPU利用率可忽略不計(jì)。我們的獨(dú)角獸工人現(xiàn)在有更多的資源來(lái)提供網(wǎng)頁(yè)請(qǐng)求。此外,對(duì)Kafka和磁盤的寫入現(xiàn)在已經(jīng)被緩存,因此總體壓力較小。
多租戶:這大大降低了運(yùn)營(yíng)成本,因?yàn)槲覀儾渴饐蝹€(gè)實(shí)例,監(jiān)控單個(gè)服務(wù),執(zhí)行單個(gè)部署,在一個(gè)地方更新配置。
維護(hù):新的結(jié)構(gòu)比以前更簡(jiǎn)單,其中包括Unicorn,Rack,中間件,Rails,nginx,Varnish&Memcached。現(xiàn)在只有標(biāo)準(zhǔn)的庫(kù),兩個(gè)簡(jiǎn)答的外部包和HAproxy。新結(jié)構(gòu)中的更少層次意味著調(diào)試也變得更加容易。
該服務(wù)可以從其他前端(即與主Rails應(yīng)用程序完全不同的應(yīng)用程序)重用。
當(dāng)我們部署抓捕時(shí),我們注意到主要應(yīng)用程序響應(yīng)時(shí)間有所增加:
Rails應(yīng)用程序響應(yīng)時(shí)間
這是預(yù)料之中。 與常規(guī)網(wǎng)頁(yè)請(qǐng)求相比,跟蹤請(qǐng)求非常快。 與多個(gè)數(shù)據(jù)庫(kù)查詢,ElasticSearch查詢,渲染Rails模板等相比,對(duì)Kafka和文件系統(tǒng)的寫入工作很少,因此我們的NewRelic圖形更能代表用戶的實(shí)際體驗(yàn)。
最后,一些性能指標(biāo)(假設(shè)一個(gè)單一的scratchd實(shí)例):
平均。 響應(yīng)時(shí)間:1ms
當(dāng)前傳輸:7k請(qǐng)求/分
估計(jì)容量:?60k請(qǐng)求/分鐘
內(nèi)存占用:61MB
GC:470μs平均 暫停時(shí)間,1.2ms累計(jì)時(shí)間超過(guò)5min
CPU使用量可忽略
下一步是什么?
新方法解決了我們?cè)谶^(guò)去實(shí)施中遇到的問(wèn)題,并帶來(lái)了額外的好處。這些不是免費(fèi)的,當(dāng)然,因?yàn)槲覀冎Ц读诵路?wù)所需的儀器的成本(即監(jiān)控,配置,部署)。這是我們高興地采取的權(quán)衡,因?yàn)槲覀儸F(xiàn)在擁有一個(gè)更可靠,可維護(hù)和高效的系統(tǒng)。
也就是說(shuō),還有待完成的工作。我們計(jì)劃添加基準(zhǔn)測(cè)試,集成依賴管理工具,提高性能,指定部署過(guò)程,并建立組織中任何Go服務(wù)應(yīng)達(dá)到的一些標(biāo)準(zhǔn)。
我們非常高興能夠在生產(chǎn)中運(yùn)行。這是我們?cè)谏a(chǎn)中使用Go的第一步,揭示了這樣一個(gè)系統(tǒng)的樣子。我們探索了各種方法,并獲得了在Go生態(tài)系統(tǒng)中如何訪問(wèn)日志記錄,配置,測(cè)試和代碼架構(gòu)的經(jīng)驗(yàn),這使我們有信心使用該語(yǔ)言來(lái)解決其他問(wèn)題。
總結(jié)
以上是生活随笔為你收集整理的用Go重构WEB请求分析跟踪服务的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 虚拟服务器备案流程,国内虚拟主机备案流程
- 下一篇: 分析各种排序算法的优劣