偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Golang 后臺服務性能優(yōu)化,實用 Tips 梳理大全

開發(fā)
本文列舉了很多常用的tips,基本都是我日常開發(fā)中遇到的問題,我將這些問題和方法梳理了下來。

作者 | rossixiao

性能優(yōu)化是一個經(jīng)久不衰的課題了,我們都常做。本文列舉了很多常用的Tips,基本都是我日常開發(fā)中遇到的問題,我將這些問題和方法梳理了下來。

一、GC原理

這一節(jié)會較為詳細介紹go的內存管理機制,也即GC。之所以要重點介紹是因為:

  • GC是很多服務的性能瓶頸,在性能優(yōu)化問題上是舉足輕重的。
  • 許多常見的優(yōu)化手段都是圍繞著內存管理進行的,只有了解了原理處理起來才游刃有余。另外go自身的內存管理方案也一直在迭代優(yōu)化,了解后我們可能會發(fā)現(xiàn)自己遇到的性能問題是因為go版本太低了。
  • go這一套內存自動管理方案本身很有借鑒意義,如果學習到,碰到相似的業(yè)務可以效仿其設計方案。如一些復雜的調度系統(tǒng)。

二、標記-清除(Stop-the-world)

STW是指暫停所有goroutine,標記可達和不可達的對象,最后清除不可達對象,完成垃圾回收的過程。 在go1.3之前垃圾回收就是依賴的全局STW,因此性能很低,一次STW帶來的停頓時間可達數(shù)百毫秒。

三、三色標記法+寫屏障

1. 三色標記法

(1) 初始時所有的對象節(jié)點都被標記成白色

(2) 第一次掃描從根節(jié)點出發(fā),把能遍歷到的對象從白色集合放到灰色集合

(3) 重復掃描灰色集合,將灰色對象引用的對象從白色集合放到灰色集合,然后把此灰色對象放到黑色集合

(4) 直到灰色集合清空,內存中只有黑白兩種顏色。此時可以回收所有白色對象

很多人把三色標記法稱為三色并發(fā)標記法,因為它存儲了對象的中間狀態(tài),不需要一次性遍歷完。但實際上和程序并發(fā)運行時,對象之間的引用關系會發(fā)生更改(寫操作),而染色會讀引用關系,也即發(fā)生了讀寫沖突。這種沖突可能導致白色對象斷開和灰色對象的鏈接,掛在一個黑色對象上,而黑色對象是不會作為掃描的根節(jié)點的,因此白色對象被誤刪除,如圖中的對象3。因此內存回收的一個很關鍵的操作就是把白色對象保護起來,可以延時刪除,但不能誤刪除。

2. 觸發(fā)三色標記法不安全的必要條件

  • 白色對象被黑色對象引用
  • 且白色對象斷開了所有灰色對象與它之間的可達關系

其實只要破壞了這兩個必要條件之一,就能避免白色對象被誤刪除。后面的優(yōu)化都是圍繞著這個規(guī)則來的。

3. 加入寫屏障,保護白色節(jié)點

  • 插入屏障:將B節(jié)點掛在A的下游時,B節(jié)點會被標記為灰色。 保障了白色節(jié)點不會被掛在黑色節(jié)點下。
  • 刪除屏障:對刪除的對象,如果自身為白色,會被標記為灰色。保障了被刪除的白色節(jié)點有灰色節(jié)點與之鏈接(對,自己給自己撐腰)。

go1.5就升級到了三色標記法+寫屏障的策略,保證了掃描和程序可以并發(fā)執(zhí)行,無需停頓。但由于棧寫操作頻繁且要保障運行效率,寫屏障只運用到了堆上,如果白色節(jié)點被掛在黑色節(jié)點上,為了保障安全性,棧還是要進行一次STW掃描,以修正狀態(tài)。這一STW停頓一般在10~100ms。

四、混合寫屏障

go1.8引入了混合寫屏障,避免了對棧的重復掃描,極大減少了STW的時間。和寫屏障對比,加了以下兩個操作:

  • GC開始時會將棧上的所有可達對象標記為黑色
  • GC期間,棧上新創(chuàng)建的對象會被初始化為黑色

這樣做的道理在,棧的可達對象全標黑了,受顏色保護。而也不會出現(xiàn)白色(不可達)對象被掛在黑色對象的情況,因為它,不可達。

引入混合寫之后,以及幾乎不需要STW了。

五、GC優(yōu)化

1. GC瓶頸分析

(1) 癥結在GC掃描

已知go已經(jīng)把STW壓縮到極致了,所以這并非是大多數(shù)系統(tǒng)的問題所在,真正消耗性能的是gc掃描的計算過程。和GC回收相比也是掃描過程更消耗cpu。

掃描的時機:

  • 堆內存達到閾值時觸發(fā)。下次GC閾值 = 上次GC后存活對象大小 × (1 + GOGC/100),默認 GOGC=100(內存翻倍時觸發(fā)),可通過環(huán)境變量調整。
  • 定時觸發(fā)。若持續(xù) 2 分鐘未觸發(fā) GC,強制啟動掃描(避免長期未回收的內存泄漏)。
  • 手動觸發(fā)。調用 runtime.GC() 強制啟動掃描,常用于調試或內存敏感操作后。
  • 內存分配時觸發(fā) 。申請大對象(>32KB)或小對象時本地緩存不足(mcache 耗盡),可能觸發(fā)掃描。

內存回收的時機:

  • 標記終止后立即啟動 。標記階段完成后,清除階段回收所有白色(未標記)對象,此階段與用戶代碼并發(fā)執(zhí)行。
  • 內存分配時觸發(fā)輔助回收 。若程序在 GC 過程中分配新內存,可能被要求協(xié)助執(zhí)行部分回收任務。一般來講,gc掃描是更加消耗性能的那一步,但我們一般不分開說,統(tǒng)稱一次gc。

(2) 如何定位GC問題

利用火焰圖,可以看到gcBgMarkWorker占用cpu的百分比,一般超過10%就需要優(yōu)化了。 需要留意的是mallocgc屬于內存分配帶來的瓶頸,并非GC掃描問題。

2. 減少堆對象分配

GC優(yōu)化的方向之一是減少堆對象的分配,這是因為和棧相比,堆對象要GC掃描的時候要遞歸掃描所有對象,且棧對象會隨著生命周期的結束而被釋放,而堆對象全部需要gc掃描來回收。

(1) 小對象使用結構體而非指針

func createUser() *User {
    return &User{ID: 1, Name: "Alice"} // 逃逸到堆
}

改成下面的寫法編成棧分配,隨著生命周期被自動回收:

func createUser() User {
    return User{ID: 1, Name: "Alice"} // 棧分配,函數(shù)結束后自動回收
}

(2) 通過參數(shù)傳遞替代閉包捕獲

func main() {
    x := 42
    go func() {
        fmt.Println(x) // x 逃逸到堆
    }()
}
func main() {
    x := 42
    go func(val int) {
        fmt.Println(val) // val 通過值傳遞,保留在棧上
    }(x)
}

(3) 利用bigcache存儲大對象(GB級別)

bigcache利用[]byte數(shù)組存儲對象,會被當成是一個整體只會掃描一次,完全規(guī)避了gc掃描問題。但需要自己做內存管理,且使用的時候要加一次編解碼操作。

3. 內存池減少對象分配

var pool = sync.Pool{
    New: func() interface{} { return make([]byte, 1024) },
}
func processRequest() {
    buf := pool.Get().([]byte) // 從池中獲?。赡軓陀茫?    defer pool.Put(buf)        // 放回池中
    // 使用 buf...
}

4. 減少GC掃描次數(shù)

(1) 調整GO GC大小

默認是增加一倍觸發(fā)GC回收,可以適當調大:

import "runtime/debug"
func main() {
    debug.SetGCPercent(200)  // 堆增長 200% 即觸發(fā) GC
}

(2) 抬高堆大小基數(shù)

初始化的時候分配一個比較大的對象,提高觸發(fā)GC的基數(shù):

func main() {
    ballast := make([]byte, 10<<30) // 10GB 虛擬內存(實際 RSS 不增加)
    runtime.KeepAlive(ballast)     // 阻止回收
    // 主邏輯...
}

預分配內存:

// 未優(yōu)化:多次擴容
var data []int
for i := 0; i < 1000; i++ {
    data = append(data, i)  // 可能觸發(fā)多次堆分配
}

預分配內存,可以避免中途多次觸發(fā)GC掃描,且也減少了數(shù)據(jù)遷移帶來的開銷:

// 優(yōu)化:單次預分配
data := make([]int, 0, 1000)  // 一次性分配底層數(shù)組
for i := 0; i < 1000; i++ {
    data = append(data, i)    
}

(3) 為什么不能手動回收,減少GC壓力呢?

go不支持

六、善用緩存

緩存在我們這的使用場景還是挺多的,我們需要合理設計緩存,使得對外接口的平均耗時在100ms以下。我們曾經(jīng)有因為沒加緩存,在for循環(huán)中拉游戲詳情,導致大部分線程都阻塞在I/O等待中,導致接口耗時高,系統(tǒng)吞吐量低。

for appid := range appids {
 detail := GetDetailInfo(appid) // rpc調用
    ...
}

但加緩存并非是無腦加的,緩存本身也可能會到來性能問題。如:

  • 商城首頁因異步更新緩存的時候頻繁分配內存,導致cpu利用率出現(xiàn)周期性尖刺。這種也是會浪費cpu的,可以適當打散,減小尖刺。

  • 使用了bigcache,但設置的內存清理時間太長,導致期間內內存打滿引起OOM,機器頻繁重啟。

七、善用并發(fā)

1. 非關鍵路徑異步化處理

對于耗時很高的非關鍵路徑,要異步化處理,防止阻塞主流程。如一些非關鍵上報異步處理,再如暢玩好友在玩耗時較高,前端采用了異步加載的方式。

2. 盡量減少鎖競爭或者無鎖化

如果資源競爭激烈,很可能會導致鎖等待時間太長,和增加調度壓力而浪費cpu。

減少鎖的范圍:

func main() {
   lock()
   defer Unlock()
   newA,newB := get() // 復雜的賦值操作
   cache.A = newA
   cache.B = newB
}
func main() {
   newA,newB := get() 
   newCache.A = newA
   newCache.B = newB // 先賦值,再直接替換,只需要鎖住替換這一步
   Lock()
   cache = newCache
   Unlock()
}

選用讀寫鎖/樂觀鎖來優(yōu)化鎖方案:

選用原子操作替代鎖:

func main() {
 var counter int32 = 0
 // 模擬10個goroutine并發(fā)自增
 for i := 0; i < 10; i++ {
  go func() {
   atomic.AddInt32(&counter, 1) // 原子加1
  }()
 }
}

使用協(xié)程池防止OOM:盡管go的協(xié)程已經(jīng)非常輕量了,在一些場景還是要控制協(xié)程的數(shù)目,防止協(xié)程無節(jié)制得擴增,導致資源耗盡或者調度壓力大。

八、一些高性能編程的好習慣

1. 避免大日志

在一次壓測中,一個2核2G的服務,日志200k字節(jié),吞吐量只能到80tps,后排查到瓶頸在于日志打太多了,減少日志輸出后性能提升了幾十倍。

可以看到當前主要性能消耗在字符串編解碼這里。

2. 避免深度拷貝

我們應該盡量減少深度拷貝的使用,在商城首頁這里,由于過度使用了clone,導致了gc性能瓶頸。去掉clone,只對部分數(shù)據(jù)賦值后,性能提升了50%

3. 避免反射

由于每個qgame的配置項是不同的結構體,為了通用化,qgameclient最開始利用反射來獲取配置

type configItem struct {
 attr *qgame.ConfigAttr // 公共屬性
 item reflect.Value     // 配置項
}

// GetConfig 拉取配置
func GetConfig(ctx context.Context,
 qryReq *QueryReq,
 attrs map[string]*qgame.ConfigAttr,
 confs interface{}) error {
 itemType := reflect.TypeOf(confs).Elem().Elem() // 堆分配
// 獲取配置
 c, err := getConfWithCache(ctx, itemType, qryReq)
if err != nil {
return err
 }
// 解析并填充attrs和confs
 d := reflect.New(itemType)  // 堆分配
 reflectMap := reflect.ValueOf(confs) // 堆分配
for k, v := range c {
if d.Type() != v.item.Type() {
   return errs.Newf(ErrParams, "data type unmatch:%v,%v", d.Type(), v.item.Type())
  }
  attrs[k] = v.attr
  reflectMap.SetMapIndex(reflect.ValueOf(k), v.item) // 堆分配
 }
returnnil
}

后續(xù)遇到了性能瓶頸,反射需要在運行時動態(tài)檢查數(shù)據(jù)類型和創(chuàng)建臨時對象(每次reflect.ValueOf()或reflect.TypeOf()調用至少產生1次堆分配)。引入泛型,泛型在編譯時期會生成對應類型的代碼,運行時無需校驗類型和分配臨時對象。

type ConfigItem[T any] struct {
 Attr   *qgame.ConfigAttr // 公共屬性
 Config *T                // config 配置map,key為配置id
}

func (c *QgameCli[T]) GetConfig(ctx context.Context, req *QueryReq) *QueryRsp[T] {
 rsp := &QueryRsp[T]{
  Items: map[string]*ConfigItem[T]{},
  Env:   cache.env,
 }
    items := cache.getCachedConfig(req)
for key, item := range items {
  rsp.Items[key] = item
 }
return rsp
}

最后性能上:泛型>強制類型轉換/斷言>反射

4. 任務打散

當某個機器性能跟不上任務的計算復雜度時,可以考慮把計算任務打散到不同機器執(zhí)行。我們用生產消費者模式執(zhí)行任務的時候,消費者經(jīng)常利用北極星的負載均衡能力,把任務平均分配到每個機器執(zhí)行。

// StartConsume 開始消費
func StartConsume() {
 for i := 0; i < 100; i++ {
  util.GoWithRecover(func() {
   for item := range ch {
    item := item
    consumeRpc(item) // 走rpc調用,打散消費任務
   }
  })
 }
}

5. 編解碼選型

目前比較常用的編解碼類型有pb、json、yaml和sonic,編解碼性能還是相差很大的, 之前gameinfoclient序列化從json改成pb時,獲取游戲詳情的耗時從100ms優(yōu)化到了20ms。vtproto是司內大神提供的pb編解碼優(yōu)化版本,去掉了pb官方編碼中的反射過程。實踐發(fā)現(xiàn)trpc-proxy利用了vtproto,性能提高了20%。

(1) 性能對比

這是ai給出的通識結論:

我自己實測了發(fā)現(xiàn)單編碼json的編碼性能居然高于pb(sonic > json > vtproto > proto > yaml),這不是一個結論別記憶,后面會解釋:

BenchmarkProtoMarshal-16         1000000              1074 ns/op
BenchmarkVtProtoMarshal-16       1157985               903.8 ns/op
BenchmarkJsonMarshal-16          1743318               688.7 ns/op
BenchmarkSonicMarshal-16         3684892               335.9 ns/op
BenchmarkYamlMarshal-16           154058              7499 ns/op
測試對象:
TestData := &pb.TestStruct{
  A: 1,
  B: []string{
   "test",
  },
  C: map[int32]string{
   1: "test",
  },
}

實際上是因為對于小對象而言json的反射機制開銷較小,且go1.22版本優(yōu)化了json反射機制,性能有所提升,所以表現(xiàn)為小對象 json的編碼性能高于proto。 我嘗試換成大對象,印證了這一點(sonic > vtproto > pb > json > yaml)

BenchmarkProtoMarshal-16             912           1325582 ns/op
BenchmarkVtProtoMarshal-16           908           1316555 ns/op
BenchmarkJsonMarshal-16              666           1792485 ns/op
BenchmarkSonicMarshal-16            4705            252298 ns/op
BenchmarkYamlMarshal-16               79          13914111 ns/op
測試對象:
a := int32(12345)
b := make([]string, 0, 5000)
for i := 0; i < 5000; i++ {
 b = append(b, fmt.Sprintf("str-%d-abcdefghijklmnopqrstuvwxyz", i))
}
c := make(map[int32]string, 3000)
for i := int32(0); i < 3000; i++ {
 c[i] = fmt.Sprintf("value-%d-0123456789ABCDEF", i) 
}
TestData = &TestDataStruct{
 A: a,
 B: b,
 C: c,
}

最后,一般編碼和解碼是對稱使用的,這里也測了一下對稱使用編解碼的性能:

小對象:
BenchmarkProto-16                 560961              2082 ns/op
BenchmarkVtProto-16               559299              2011 ns/op
BenchmarkJson-16                  375512              3248 ns/op
BenchmarkSonic-16                1224876               974.7 ns/op
BenchmarkYaml-16                   62400             18995 ns/op
 
大對象:
BenchmarkProto-16                    369           3260424 ns/op
BenchmarkVtProto-16                  364           3349230 ns/op
BenchmarkJson-16                     201           5934529 ns/op
BenchmarkSonic-16                   1483            798178 ns/op
BenchmarkYaml-16                      42          28831239 ns/op

至此我的結論是: sonic性能最卓越,如果對壓縮大小、平臺不敏感,能使用sonic盡量使用sonic;如果對可讀性要求比較高用json/yaml;跨端通信用pb,對性能敏感可升級用vtproto。

6. 字符串拼接

  • strings.Builder性能最高,底層是[]byte,可動態(tài)擴容
  • strings.Join底層是strings.Builder,一次性計算分配內存,性能差不多
  • +運算符。需要不斷創(chuàng)建新的臨時對象
  • fmt.Sprintf()。性能很差,涉及到反射。性能敏感場景避免使用。

九、利用工具排查性能問題

1. pprof

也即火焰圖,伽利略已經(jīng)集成了火焰圖插件,可直接使用。下面一個例子是分析game_switch服務的性能瓶頸:

查看cpu time。發(fā)現(xiàn)SsoGetShareTails 、編解碼、 GcBgMarkWorker占用了大部分cpu時間。

繼續(xù)往下看,分析出SsoGetShareTails主要消耗在Sprintf函數(shù)上。

到這里已經(jīng)可以猜測出:

  • 編解碼占大部分推測出可能回包包體很大,影響了性能。
  • gcBgMarkWorker表示gc掃描消耗的性能,可能頻繁分配內存,或者內存占用比較高。
  • SsoGetShareTails中的Sprintf前面已經(jīng)提到過是一個性能很低的字符串拼接方案,可以直接優(yōu)化掉。
  • 繼續(xù)查看堆內存大小,包含gc回收的??梢远ㄎ坏酱箢^在接口、序列化和壓縮上。

代碼定位如下圖。序列化和壓縮是框架自帶的,符合前面的推斷---回包太大導致的。

最后看看還存活的堆內存大小。發(fā)現(xiàn)是游戲詳情的緩存,符合預期。雖然在存活的堆內存里它算大頭,但和總的堆內存大小對比還是挺小的,不是目前主要優(yōu)化點,可降低優(yōu)先級后續(xù)優(yōu)化。

2. trace

對于一般的程序,pprof已經(jīng)夠用了。如果要更精細得定位問題,可以使用trace,和pprof不同的是,pprof是基于統(tǒng)計維度的,原理是定期采樣生成cpu和內存的快照,而trace直接追蹤到整個程序的運行,能提供時間線上的事件流。像這樣:

上面提供是一段有死鎖的代碼:

func main() {
// 創(chuàng)建trace文件
 f, err := os.Create("deadlock_trace.out")
if err != nil {
  log.Fatal(err)
 }
defer f.Close()

// 啟動trace
 err = trace.Start(f)
if err != nil {
  log.Fatal(err)
 }
defer trace.Stop()

// 創(chuàng)建兩個互斥鎖
var mutex1, mutex2 sync.Mutex

// goroutine1 先鎖mutex1,再嘗試鎖mutex2
 f1 := func() {
  mutex1.Lock()
  log.Println("goroutine1 獲得 mutex1")
  time.Sleep(1 * time.Second) // 確保死鎖發(fā)生
  mutex2.Lock()
  log.Println("goroutine1 獲得 mutex2")
  mutex2.Unlock()
  mutex1.Unlock()
 }
go f1()

// goroutine2 先鎖mutex2,再嘗試鎖mutex1
gofunc() {
  mutex2.Lock()
  log.Println("goroutine2 獲得 mutex2")
  time.Sleep(1 * time.Second) // 確保死鎖發(fā)生
  mutex1.Lock()
  log.Println("goroutine2 獲得 mutex1")
  mutex1.Unlock()
  mutex2.Unlock()
 }()

for i := 0; i < 10; i++ {
gofunc() {
   time.Sleep(5 * time.Second)
  }()
 }
// 等待足夠時間讓死鎖發(fā)生
for {

 }
}

通過 Goroutine analysis,可以看到func1對應的協(xié)程編號是23,且大部分時間都處于阻塞中:

點擊查看協(xié)程23具體的事件流,func1最后一次執(zhí)行停留在sleep這,雖然很疑惑為什么不在Lock()這里,但也印證了后續(xù)的流程被阻塞了

十、需求階段

最近被問到:“如果下游接口就是很慢,你要怎么辦?”。最近暢玩就遇到了類似的問題,暢玩要接入ams廣告,但一個廣告接口卻有接近800ms的耗時,明顯對體驗是有損的,使得我們不得不推動下游做性能優(yōu)化。除此之外也提醒了我不應該僅限于需求開發(fā),而應該從用戶的角度出發(fā),思考需求是否合理、是否可優(yōu)化,協(xié)同產品一起保障體驗。

1. 體驗要穩(wěn)定

(1) 做好兜底

  • 異常兜底。
  • 如算法側掛了,要展示兜底素材;游戲封面缺失,過濾不展示或者展示兜底封面。邊界兜底。

如帖子瀏覽完了的文案兜底;完善錯誤碼機制,提示用戶當前狀態(tài)。

(2) 穩(wěn)定排序

有時候產品會忘記提供排序策略,除了被指定的隨機資源位,其他應該協(xié)調一個排序方案,避免每次刷新都展示不同的內容。

(3) 數(shù)據(jù)源一致性

如前段時間遇到的不同頁面展示的可領取禮包數(shù)不一致,實際是一個頁面展示的所有禮包,一個頁面沒展示貴族禮包,這要求我們開發(fā)前和產品溝通數(shù)據(jù)和哪個需求的頁面保持一致。

2. 畫面要流暢

(1) 數(shù)據(jù)分頁/分屏

如果一次請求的數(shù)據(jù)量太多,不僅會給后臺帶來性能問題,也可能引起前端前端渲染卡頓了,所以必要時需要溝通設計分頁或者分屏(如設計列表頁和二級頁)。

(2) 隱藏高耗時的數(shù)據(jù)

延遲加載非首屏內容,優(yōu)先保障核心功能可快速操作。

(3) 資源大小控制

如資料小卡的游戲段位圖,一開始誤給了一個10241024的高清圖,會導致UI加載很慢,后續(xù)改成了120120的就能滿足清晰度要求。

(4) 數(shù)據(jù)實時性分級

區(qū)分強實時性和弱實時性。如對于禮包數(shù)量、游戲在玩人數(shù)等允許有一段時間的延時,以減少對db的壓力。

(5) 用戶操作限頻

如已經(jīng)預約后按鈕置灰、限制點擊次數(shù)等,可以減少接口調用量,也能提高系統(tǒng)用戶吞吐量。

(6) 成本溝通

如周報tips,產品要求每個人都生成特有的圖片,已知圖像處理會帶來很多額外的開銷,應該提醒產品并切換其他方案。

(7) 回包大小控制

回包太大不僅會影響服務性能,也會加大網(wǎng)絡傳輸耗時和前端加載耗時。

責任編輯:趙寧寧 來源: 騰訊技術工程
相關推薦

2023-03-08 18:43:50

GPU模型隔離

2011-07-19 10:46:49

Windows 7優(yōu)化

2012-12-24 09:55:15

JavaJava WebJava優(yōu)化

2010-01-08 09:43:23

SQL Server分Analysis Se

2021-05-19 08:04:11

ASP.Net服務性原則

2022-05-31 10:51:12

架構技術優(yōu)化

2021-11-18 10:05:35

Java優(yōu)化QPS

2009-11-05 10:45:58

WCF服務

2023-11-19 23:24:21

Golang開發(fā)

2022-11-10 08:16:19

java性能服務性能

2017-09-26 14:56:57

MongoDBLBS服務性能

2021-07-06 12:07:27

Go 服務性能

2012-04-26 14:08:52

2019-01-22 08:45:57

SQL語句策略

2021-06-30 10:16:54

微服務架構測試

2019-09-26 08:40:27

SqliteTips優(yōu)化

2009-11-06 17:10:34

WCF服務性能計數(shù)器

2011-08-25 09:32:30

Visual Stud

2015-12-30 19:19:37

云存儲

2011-07-22 09:50:34

云服務云計算
點贊
收藏

51CTO技術棧公眾號