hello,大家好呀,我是小樓。
汕尾網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)建站!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、APP開(kāi)發(fā)、響應(yīng)式網(wǎng)站建設(shè)等網(wǎng)站項(xiàng)目制作,到程序開(kāi)發(fā),運(yùn)營(yíng)維護(hù)。創(chuàng)新互聯(lián)建站從2013年開(kāi)始到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來(lái)保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)建站。
前幾天不是寫(xiě)了這篇文章《發(fā)現(xiàn)一個(gè)開(kāi)源項(xiàng)目?jī)?yōu)化點(diǎn),點(diǎn)進(jìn)來(lái)就是你的了》嘛。
文章介紹了Sentinl的自適應(yīng)緩存時(shí)間戳算法,從原理到實(shí)現(xiàn)都手把手解讀了,而且還發(fā)現(xiàn)Sentinel-Go還未實(shí)現(xiàn)這個(gè)自適應(yīng)算法,于是我就覺(jué)得,這簡(jiǎn)單啊,把Java代碼翻譯成Go不就可以混個(gè)PR?
甚至在文章初稿中把這個(gè)描述為:「有手就可以」,感覺(jué)不太妥當(dāng),后來(lái)被我刪掉了。
過(guò)了幾天,我想去看看有沒(méi)有人看了我的文章真的去提了個(gè)PR,發(fā)現(xiàn)仍然是沒(méi)有,心想,可能是大家太忙(懶)了吧。
于是準(zhǔn)備自己來(lái)實(shí)現(xiàn)一遍,周末我拿出電腦試著寫(xiě)一下這段代碼,結(jié)果被當(dāng)頭一棒敲醒,原來(lái)這代碼不好寫(xiě)啊。
先簡(jiǎn)單介紹一下我當(dāng)時(shí)是如何實(shí)現(xiàn)的。
首先,定義了系統(tǒng)的四種狀態(tài):
const (
UNINITIALIZED = iota
IDLE
PREPARE
RUNNING
)
這里為了讓代碼更加貼近Go的習(xí)慣,用了iota
。
用了4種狀態(tài),第一個(gè)狀態(tài)UNINITIALIZED
是Java版里沒(méi)有的,因?yàn)镴ava在系統(tǒng)初始化時(shí)默認(rèn)就啟動(dòng)了定時(shí)緩存時(shí)間戳線程。
但Go版本不是這樣的,它有個(gè)開(kāi)關(guān),當(dāng)開(kāi)關(guān)開(kāi)啟時(shí),會(huì)調(diào)用StartTimeTicker
來(lái)啟動(dòng)緩存時(shí)間戳的協(xié)程,所以當(dāng)沒(méi)有初始化時(shí)是需要直接返回系統(tǒng)時(shí)間戳,所以這里多了一個(gè)UNINITIALIZED
狀態(tài)。
然后我們需要能夠統(tǒng)計(jì)QPS的方法,這塊直接抄Java的實(shí)現(xiàn),由于不是重點(diǎn),但又怕你不理解,所以直接貼一點(diǎn)代碼,不想看可以往下劃。
定義我們需要的BucketWrap:
type statistic struct {
reads uint64
writes uint64
}
func (s *statistic) NewEmptyBucket() interface{} {
return statistic{
reads: 0,
writes: 0,
}
}
func (s *statistic) ResetBucketTo(bucket *base.BucketWrap, startTime uint64) *base.BucketWrap {
atomic.StoreUint64(&bucket.BucketStart, startTime)
bucket.Value.Store(statistic{
reads: 0,
writes: 0,
})
return bucket
}
獲取當(dāng)前的Bucket:
func currentCounter(now uint64) (*statistic, error) {
if statistics == nil {
return nil, fmt.Errorf("statistics is nil")
}
bk, err := statistics.CurrentBucketOfTime(now, bucketGenerator)
if err != nil {
return nil, err
}
if bk == nil {
return nil, fmt.Errorf("current bucket is nil")
}
v := bk.Value.Load()
if v == nil {
return nil, fmt.Errorf("current bucket value is nil")
}
counter, ok := v.(*statistic)
if !ok {
return nil, fmt.Errorf("bucket fail to do type assert, expect: *statistic, in fact: %s", reflect.TypeOf(v).Name())
}
return counter, nil
}
獲取當(dāng)前的QPS:
func currentQps(now uint64) (uint64, uint64) {
if statistics == nil {
return 0, 0
}
list := statistics.ValuesConditional(now, func(ws uint64) bool {
return ws <= now && now < ws+uint64(bucketLengthInMs)
})
var reads, writes, cnt uint64
for _, w := range list {
if w == nil {
continue
}
v := w.Value.Load()
if v == nil {
continue
}
s, ok := v.(*statistic)
if !ok {
continue
}
cnt++
reads += s.reads
writes += s.writes
}
if cnt < 1 {
return 0, 0
}
return reads / cnt, writes / cnt
}
當(dāng)我們有了這些準(zhǔn)備后,來(lái)寫(xiě)核心的check邏輯:
func check() {
now := CurrentTimeMillsWithTicker(true)
if now-lastCheck < checkInterval {
return
}
lastCheck = now
qps, tps := currentQps(now)
if state == IDLE && qps > hitsUpperBoundary {
logging.Warn("[time_ticker check] switches to PREPARE for better performance", "reads", qps, "writes", tps)
state = PREPARE
} else if state == RUNNING && qps < hitsLowerBoundary {
logging.Warn("[time_ticker check] switches to IDLE due to not enough load", "reads", qps, "writes", tps)
state = IDLE
}
}
最后是調(diào)用check的地方:
func StartTimeTicker() {
var err error
statistics, err = base.NewLeapArray(sampleCount, intervalInMs, bucketGenerator)
if err != nil {
logging.Warn("[time_ticker StartTimeTicker] new leap array failed", "error", err.Error())
}
atomic.StoreUint64(&nowInMs, uint64(time.Now().UnixNano())/unixTimeUnitOffset)
state = IDLE
go func() {
for {
check()
if state == RUNNING {
now := uint64(time.Now().UnixNano()) / unixTimeUnitOffset
atomic.StoreUint64(&nowInMs, now)
counter, err := currentCounter(now)
if err != nil && counter != nil {
atomic.AddUint64(&counter.writes, 1)
}
time.Sleep(time.Millisecond)
continue
}
if state == IDLE {
time.Sleep(300 * time.Millisecond)
continue
}
if state == PREPARE {
now := uint64(time.Now().UnixNano()) / unixTimeUnitOffset
atomic.StoreUint64(&nowInMs, now)
state = RUNNING
continue
}
}
}()
}
自此,我們就實(shí)(抄)現(xiàn)(完)了自適應(yīng)的緩存時(shí)間戳算法。
先編譯一下,咚,報(bào)錯(cuò)了:import cycle not allowed!
啥意思呢?循環(huán)依賴了!
我們的時(shí)間戳獲取方法在包util
中,然后我們使用的統(tǒng)計(jì)QPS相關(guān)的實(shí)現(xiàn)在base
包中,util包依賴了base包,這個(gè)很好理解,反之,base包也依賴了util包,base包主要也使用了CurrentTimeMillis
方法來(lái)獲取當(dāng)前時(shí)間戳,我這里截個(gè)圖,但不止這些,有好幾個(gè)地方都使用到了:
但我寫(xiě)代碼時(shí)是特地繞開(kāi)了循環(huán)依賴,也就是util中調(diào)用base包中的方法是不會(huì)反向依賴回來(lái)形成環(huán)的,為此還單獨(dú)寫(xiě)了個(gè)方法:
使用新方法,就不會(huì)形成依賴環(huán)。但實(shí)際上編譯還是通過(guò)不了,這是因?yàn)镚o在編譯時(shí)就直接禁止了循環(huán)依賴。
那我就好奇了啊,Java是怎么實(shí)現(xiàn)的?
這是com.alibaba.csp.sentinel.util
包
這是com.alibaba.csp.sentinel.slots.statistic.base
包
Java也出現(xiàn)了循環(huán)依賴,但它沒(méi)事!
這瞬間勾起了我的興趣,如果我讓它運(yùn)行時(shí)形成依賴環(huán),會(huì)怎么樣呢?
簡(jiǎn)單做個(gè)測(cè)試,搞兩個(gè)包,互相調(diào)用,比如pk1
和pk2
的code
方法都調(diào)用對(duì)方:
package org.newboo.pk1;
import org.newboo.pk2.Test2;
public class Test1 {
public static int code() {
return Test2.code();
}
public static void main(String[] args) {
System.out.println(code());
}
}
編譯可以通過(guò),但運(yùn)行報(bào)錯(cuò)棧溢出了:
Exception in thread "main" java.lang.StackOverflowError
at org.newboo.pk1.Test1.code(Test1.java:7)
at org.newboo.pk2.Test2.code(Test2.java:7)
...
這么看來(lái)是Go編譯器做了校驗(yàn),強(qiáng)制不允許循環(huán)依賴。
說(shuō)到這里,其實(shí)Java里也有循環(huán)依賴校驗(yàn),比如:Maven
不允許循環(huán)依賴,比如我在sentinel-core模塊中依賴sentinel-benchmark,編譯時(shí)就直接報(bào)錯(cuò)。
再比如SpringBoot2.6.x默認(rèn)禁用循環(huán)依賴,如果想用,還得手動(dòng)打開(kāi)才行。
Java中強(qiáng)制禁止的只有maven,語(yǔ)言層面、框架層面基本都沒(méi)有趕盡殺絕,但Go卻在語(yǔ)言層面強(qiáng)制不讓使用。
這讓我想起了之前在寫(xiě)Go代碼時(shí),Go的鎖不允許重入,經(jīng)常寫(xiě)出死鎖代碼。這擱Java上一點(diǎn)問(wèn)題都沒(méi)有,當(dāng)時(shí)我就沒(méi)想通,為啥Go不支持鎖的重入。
現(xiàn)在看來(lái)可能的原因:一是Go的設(shè)計(jì)者有代碼潔癖,想強(qiáng)制約束大家都有良好的代碼風(fēng)格;二是由于Go有循環(huán)依賴的強(qiáng)制檢測(cè),導(dǎo)致鎖重入的概率變小。
但這終究是理想狀態(tài),往往在實(shí)施起來(lái)的時(shí)候令人痛苦。
反觀Java,一開(kāi)始沒(méi)有強(qiáng)制禁用循環(huán)依賴,導(dǎo)致后面基本不可避免地寫(xiě)出循環(huán)依賴的代碼,SpringBoot認(rèn)為這是不好的,但又不能強(qiáng)制,只能默認(rèn)禁止,但如果你真的需要,也還是可以打開(kāi)的。
但話又說(shuō)回來(lái),循環(huán)依賴真的「丑陋」嗎?我看不一定,仁者見(jiàn)仁,智者見(jiàn)智。
問(wèn)題是這么個(gè)問(wèn)題,可能大家都有不同的觀點(diǎn),或是吐槽Go,或是批判Java,這都不是重點(diǎn),重點(diǎn)是我們還得在Go的規(guī)則下解決問(wèn)題。
如何解決Go的循環(huán)依賴問(wèn)題呢?稍微查了一下資料,大概有這么幾種方法:
將兩個(gè)包合成一個(gè),這是最簡(jiǎn)單的方法,但這里肯定不行,合成一個(gè)這個(gè)PR鐵定過(guò)不了。
抽取公共底層方法,雙方都依賴這個(gè)底層方法。比如這里,我們把底層方法抽出來(lái)作為common,util和base同時(shí)依賴它,這樣util和base就不互相依賴了。
---- util
---- ---- common
---- base
---- ---- common
這個(gè)方法也是最常見(jiàn),最正規(guī)的方法。
但在這里,似乎也不好操作。因?yàn)楂@取時(shí)間戳這個(gè)方法已經(jīng)非常底層了,沒(méi)辦法抽出一個(gè)和統(tǒng)計(jì)QPS共用的方法,反正我是沒(méi)能想出來(lái),如果有讀者朋友可以做到,歡迎私聊我,真心求教。
花了很多時(shí)間,還是沒(méi)能搞定。當(dāng)時(shí)的感覺(jué)是,這下翻車了,這題可沒(méi)那么簡(jiǎn)單??!
這個(gè)方法比較難想到,我也是在前兩個(gè)方法怎么都搞不定的情況下咨詢了組里的Go大佬才知道。
仔細(xì)看獲取時(shí)間戳的代碼:
// Returns the current Unix timestamp in milliseconds.
func CurrentTimeMillis() uint64 {
return CurrentClock().CurrentTimeMillis()
}
這里的CurrentClock()
是什么?其實(shí)是返回了一個(gè)Clock
接口的實(shí)現(xiàn)
type Clock interface {
Now() time.Time
Sleep(d time.Duration)
CurrentTimeMillis() uint64
CurrentTimeNano() uint64
}
作者這么寫(xiě)的目的是為了在測(cè)試的時(shí)候,可以靈活地替換真實(shí)實(shí)現(xiàn)
實(shí)際使用時(shí)RealClock,也就是調(diào)用了我們正在調(diào)優(yōu)的時(shí)間戳獲??;MockClock則是測(cè)試時(shí)使用的。
這個(gè)實(shí)現(xiàn)是什么時(shí)候注入的呢?
func init() {
realClock := NewRealClock()
currentClock = new(atomic.Value)
SetClock(realClock)
realTickerCreator := NewRealTickerCreator()
currentTickerCreator = new(atomic.Value)
SetTickerCreator(realTickerCreator)
}
在util初始化時(shí),就寫(xiě)死注入了realClock。
這么一細(xì)說(shuō),是不是對(duì)循環(huán)依賴的解決有點(diǎn)眉目了?
我們的realClock實(shí)際上依賴了base,但這個(gè)realClock可以放在util包外,util包內(nèi)只留一個(gè)接口。
注入真實(shí)的realClock的地方也不能放在util的初始化中,也得放在util包外(比如Sentinel初始化的地方),這樣一來(lái),util就不再直接依賴base了。
這樣一改造,編譯就能通過(guò)了,當(dāng)然這代碼只是個(gè)示意,還需要精雕細(xì)琢。
我們發(fā)現(xiàn)就算給你現(xiàn)成的代碼,抄起來(lái)也是比較難的,有點(diǎn)類似「腦子會(huì)了,但手不會(huì)」的尷尬境地。
同時(shí)每個(gè)編程語(yǔ)言都有自己的風(fēng)格,也就是我們通常說(shuō)的,Go代碼要寫(xiě)得更「Go」一點(diǎn),所以語(yǔ)言不止是一個(gè)工具這么簡(jiǎn)單,它的背后也存在著自己的思考方式。
本文其實(shí)是從一個(gè)案例分享了如何解決Go的循環(huán)依賴問(wèn)題,以及一些和Java對(duì)比的思考,更偏向代碼工程。
如果你覺(jué)得還不過(guò)癮,也可以看看這篇文章,也是關(guān)于代碼工程的:
看完,記得點(diǎn)個(gè)關(guān)注
、贊
、在看
哦,這樣我才有動(dòng)力持續(xù)輸出優(yōu)質(zhì)技術(shù)文章 ~ 我們下期再見(jiàn)吧。
- 搜索關(guān)注微信公眾號(hào)"捉蟲(chóng)大師",后端技術(shù)分享,架構(gòu)設(shè)計(jì)、性能優(yōu)化、源碼閱讀、問(wèn)題排查、踩坑實(shí)踐。