關(guān)于Golang GC的一些誤解,真的比Java算法更領(lǐng)先嗎?
首先強調(diào)下本文的起因是在高可用架構(gòu)后花園群的一次聊天,大家在爭論Golang的GC到底是類似Java的ZGC還是類似Java的CMS GC。我個人的看法是Golang的GC是類似于Java的CMS GC,官方的mgc的注釋這么說的:
- // The GC runs concurrently with mutator threads, is type accurate (aka precise), allows multiple
- // GC thread to run in parallel. It is a concurrent mark and sweep that uses a write barrier. It is
- // non-generational and non-compacting. Allocation is done using size segregated per P allocation
- // areas to minimize fragmentation while eliminating locks in the common case.
其中mutator是指我們的應(yīng)用程序,因為可能會改變內(nèi)存的狀態(tài),所以命名為mutator。這段話翻譯過來,大概的意思就是說Go的GC使用的是一種非分代的沒有整理過程的Concurrent Mark and Sweep算法(CMS算法),我個人再補充下,標記過程(即Mark過程)是使用三色標記法。討論的過程中出現(xiàn)兩個錯誤觀點,一個是CMS算法一定是分代的,另一個是使用了三色標記法就是類似于ZGC的做法(我個人不知道為啥有這個觀點,當時也忘記問清楚了,可能是把三色標記法和ZGC里的指針染色搞混了)。至于CMS是否一定要分代,我給一篇介紹,再借用R大的一句話給問題先做個結(jié)論,“只要不移動對象做并發(fā)GC,最終就會得到某種形式的CMS。”
標記-清理算法
標記-清理算法是一種追蹤式的垃圾回收算法,并不會在對象死亡后立即將其清理掉,而是在一定條件下觸發(fā),統(tǒng)一校驗系統(tǒng)中的存活對象,進行回收工作。
標記-清理分為兩個部分,標記和清理,標記過程會遍歷所有對象,查找出死亡對象。通過GC ROOT到對象的可達性就可以確認對象的存活,也就是說,如果存在一條從GC ROOT出發(fā)的引用最終可指向某個對象,就認為這個對象是存活的。這樣,未能證明存活的對象就可以標記為死亡了。標記結(jié)束后,再次進行遍歷,清理掉確認死亡的對象。
標記清理都是并發(fā)執(zhí)行的標記-清理算法就是CMS。三色標記法是一種標記對象使用的算法。
Go GC的改進歷史
- 1.3以前的版本使用標記-清理的方式,整個過程都需要STW。
- 1.3版本分離了標記和清理的操作,標記過程STW,清理過程并發(fā)執(zhí)行。
- 1.5版本在標記過程中使用三色標記法?;厥者^程主要有四個階段,其中,標記和清理都并發(fā)執(zhí)行的,但標記階段的前后需要STW一定時間來做GC的準備工作和棧的re-scan。
- 1.8版本在引入混合屏障rescan來降低mark termination的時間
GC 流程 1.5
- Sweep Termination: 收集根對象,清理上一輪未清掃完的span,啟用寫屏障和輔助GC,輔助GC將一定量的標記和清掃工作交給用戶goroutine來執(zhí)行,寫屏障在后面會詳細說明。
- Mark: 掃描所有根對象和通過根對象可達的對象,并標記它們
- Mark Termination: 完成標記工作,重新掃描部分根對象(要求STW),關(guān)閉寫屏障和輔助GC
- Sweep: 按標記結(jié)果清理對象
GC 1.8
1.8引入混合屏障,最小化第一次STW,混合屏障是指:
寫入屏障,在寫入指針f時將C對象標記為灰色。Go1.5版本使用的Dijkstra寫屏障就是這個原理,偽代碼如下:
- writePointer(slot, ptr):
- shade(ptr)
- *slot = ptr
刪除屏障,使用的Yuasa屏障偽代碼如下:
- writePointer(slot, ptr):
- if (isGery(slot) || isWhite(slot))
- shade(*slot)
- *slot = ptr
1.8中引入的混合屏障,寫入屏障和刪除屏障各有優(yōu)缺點,Dijkstra寫入寫屏障在標記開始時無需STW,可直接開始,并發(fā)進行,但結(jié)束時需要STW來重新掃描棧,標記棧上引用的白色對象的存活;Yuasa刪除屏障則需要在GC開始時STW掃描堆棧來記錄初始快照,這個過程會記錄開始時刻的所有存活對象,但結(jié)束時無需STW。Go1.8版本引入的混合寫屏障結(jié)合了Yuasa的刪除寫屏障和Dijkstra的寫入寫屏障,結(jié)合了兩者的優(yōu)點,偽代碼如下:
- writePointer(slot, ptr):
- shade(*slot)
- if current stack is grey:
- shade(ptr)
- *slot = ptr
因此,個人的理解是在Mark init階段開始的時候激活混合寫屏障這時候STW,在rescan階段應(yīng)該也只需要在去掉混合寫屏障的時候STW。從算法上來看,是接近Java CMS算法,而非ZGC,當然Go GC的比Java CMS GC有很多實現(xiàn)上的優(yōu)化。
為了了解其細節(jié),查到William有篇文章講了不少GC細節(jié),譯文如下。
在Go 1.12版本里,Go垃圾收集器依然使用非分代的并發(fā)的三色標記清理算法。Ken Fox這篇文章里關(guān)于GC的動畫非常贊。Go的垃圾收集器的實現(xiàn)隨著Go版本的變化而發(fā)生變化。因此,一旦發(fā)布下一版本,很多細節(jié)可能會有不同。
垃圾收集器行為
Go垃圾收集器的行為分為兩個大階段Mark(標記)階段和Sweep(清理)階段。Mark階段又分為三個步驟,其中兩個階段會有STW(Stop The World),另一個階段也會有延遲,從而導(dǎo)致應(yīng)用程序延遲并降低吞吐量,這三個步驟是:
- Mark Setup 階段- STW
- Marking階段- 并發(fā)執(zhí)行
- Mark終止階段 - STW
下面一一討論。
Mark Setup階段
垃圾收集開始時,必須執(zhí)行的第一個動作是打開寫屏障(Write Barrier)。寫屏障的目的是允許垃圾收集器在垃圾收集期間維護堆上的數(shù)據(jù)完整性,因為垃圾收集器和應(yīng)用程序?qū)⒉l(fā)執(zhí)行。
為了打開寫屏障,必須停止每個goroutine。此動作通常非???,平均在10到30微秒之內(nèi)完成。
上圖展示了在垃圾收集開始之前有四個goroutine在運行應(yīng)用程序。為了暫停所有的goroutine,唯一的方法是讓垃圾收集器觀察并等待每個goroutine進行函數(shù)調(diào)用。等待函數(shù)調(diào)用是為了保證goroutine停止時處于安全點。如果其中一個goroutine不進行函數(shù)調(diào)用而其他goroutine執(zhí)行函數(shù)調(diào)用,這種情況下會發(fā)生什么?
上圖展示了一個問題。在P4上運行的goroutine停止之前,垃圾收集無法啟動。然而由于P4處于如下循環(huán)中,垃圾收集器可能無法啟動。
- func add(numbers []int) int {
- var v int
- for _, n := range numbers {
- v += n
- }
- return v
- }
上面的代碼片段是P4上正在執(zhí)行的代碼。go routine的運行時間取決于slice的大小。這段代碼可以阻止垃圾收集器啟動。更糟糕的是,當垃圾收集器等待P4時,其他P也無法提供服務(wù)。所以goroutines在合理的時間范圍內(nèi)進行函數(shù)調(diào)用對于GC來說是至關(guān)重要的。
Marking階段
一旦寫屏障打開,垃圾收集器就開始標記階段。垃圾收集器所做的第一件事是占用25%CPU。垃圾收集器使用Goroutines進行垃圾收集工作,. 這意味著對于一個4線程的Go程序,一個P將專門用于垃圾收集工作。
上圖中P1專門用于垃圾收集?,F(xiàn)在垃圾收集器可以開始標記階段。標記階段需要標記在堆內(nèi)存中仍然在使用中的值。首先檢查所有現(xiàn)goroutine的堆棧,以找到堆內(nèi)存的根指針。然后收集器必須從那些根指針遍歷堆內(nèi)存圖,標記可以回收的內(nèi)存(譯者注:標記的算法就是所謂的三色標記算法)。當標記工作在P1上進行時,應(yīng)用程序可以在P2,P3和P4上繼續(xù)進行。這意味著垃圾收集器的影響已最小化到當前CPU的25%。
這是理想的情況,然而現(xiàn)實卻遠沒有如此簡單。如果在垃圾收集過程中,P1在堆內(nèi)存達到極限之前無法完成標記工作(因為應(yīng)用程序可能在大量分配內(nèi)存),該怎么辦?如果3個Goroutines中只有一個大量分配內(nèi)存導(dǎo)致P1無法完成標記工作,在這種情況下,分配新內(nèi)存的速度會變慢,特別是始作俑者的那個Go routine分配內(nèi)存的時候。
如果垃圾收集器確定需要減慢內(nèi)存分配,原本運行應(yīng)用程序Goroutines會協(xié)助標記工作。應(yīng)用程序Goroutine成為Mark Assist(協(xié)助標記)中的時間長度與它申請的堆內(nèi)存成正比。Mark Assist有助于更快地完成垃圾收集。
上圖顯示了在P3上運行的應(yīng)用程序Goroutine現(xiàn)在正在執(zhí)行Mark Assist并進行收集工作。
垃圾收集器的一個設(shè)計目標是減少對Mark Assists的需求。如果任何本次垃圾回收最終需要大量的Mark Assist才能完成工作,則垃圾收集器會提前開始下一個垃圾收集周期。這樣做可以減少下一次垃圾收集所需的Mark Assist。
Mark終止
一旦并發(fā)標記階段完成,下一個階段就是標記終止。最終關(guān)閉寫屏障,執(zhí)行各種清理任務(wù),并計算下一個垃圾回收周期的目標。一直處于循環(huán)中的goroutine也可能導(dǎo)致stw延長(類似mark setup的情況)。
上圖顯示了在標記終止階段完成時如何停止所有g(shù)oroutine。這一動作平均在60到90微秒之間完成。這個階段可以在沒有STW的情況下完成,但是使用STW的代碼更簡單。
一旦收集完成,應(yīng)用程序Goroutines就可以再次使用所有P,應(yīng)用程序?qū)⒒謴?fù)到油門全開的狀態(tài)。
上圖顯示了垃圾收集完成后,所有P現(xiàn)在都可以用于應(yīng)用程序。
并發(fā)清理
標記完成后,下一階段執(zhí)行并發(fā)清理。清理階段用于回收標記階段中標記出來的可回收的內(nèi)存。當應(yīng)用程序goroutine嘗試在堆內(nèi)存中分配新內(nèi)存時,會觸發(fā)該操作。清理導(dǎo)致的延遲和吞吐量降低被分散到每次內(nèi)存分配時。
下面是我的機器上的一個trace示例,其中有12個硬件線程可用于執(zhí)行g(shù)oroutine。
上圖顯示了部分trace快照。在這次垃圾收集過程中,三個P(總共12個P)專用于GC。你可以看到Goroutine 2450,1978和2696在這段時間里正在進行Mark Assist的工作,而不是執(zhí)行應(yīng)用程序。在Mark的最后,只有一個P專用于GC并最終執(zhí)行STW(標記終止)工作。
垃圾收集完成后,除了你看到Goroutines下面有很多玫瑰色的線條之外,應(yīng)用程序幾乎恢復(fù)全力運行。
上圖中那些玫瑰色線條代表Goroutine執(zhí)行清理工作而非執(zhí)行應(yīng)用程序。這也是Goroutine試圖分配新內(nèi)存的時刻。
上圖圖顯示了Sweep過程中Goroutines stack trace的一部分。調(diào)用runtime.mallocgc用于分配新內(nèi)存。最終調(diào)用runtime.(*mcache).nextFree 執(zhí)行清理。一旦所有可以回收的內(nèi)存都回收完畢,就不再對nextFree進行調(diào)用。
剛剛描述的行為僅在垃圾收集啟動并運行時發(fā)生。而GC百分比配置選項對于何時進行垃圾收集起著重要作用。
GC百分比
運行時中有GC Percentage的配置選項,默認情況下為100。此值表示在下一次垃圾收集必須啟動之前可以分配多少新內(nèi)存的比率。將GC百分比設(shè)置為100意味著,基于在垃圾收集完成后標記為活動的堆內(nèi)存量,下次垃圾收集前,堆內(nèi)存使用可以增加100%。
舉個例子,假設(shè)一個集合在使用中有2MB的堆內(nèi)存。
注意:使用Go時,本文中堆內(nèi)存的圖表不代表真實情況。Go中的堆內(nèi)存會碎片化。這些圖只是示意圖。
上圖顯示了最后一次垃圾完成后正在使用中的堆內(nèi)存是2MB。由于GC百分比設(shè)置為100%,因此下一次收集會在在增加2 MB堆內(nèi)存時啟動。
上圖顯示增加2MB堆內(nèi)存。這時觸發(fā)一次垃圾收集??梢詾槊看蜧C都生成GC trace,就可以查看到相關(guān)動作。
GC Trace
在運行任何Go應(yīng)用程序時,可以通過使用環(huán)境變量GODEBUG和gctrace = 1選項生成GC trace。每次發(fā)生垃圾收集時,運行時都會將GC trace信息寫入stderr。
- GODEBUG=gctrace=1 ./app
- gc 1405 @6.068s 11%: 0.058+1.2+0.083 ms clock, 0.70+2.5/1.5/0+0.99 ms cpu, 7->11->6 MB, 10 MB goal, 12 P
- gc 1406 @6.070s 11%: 0.051+1.8+0.076 ms clock, 0.61+2.0/2.5/0+0.91 ms cpu, 8->11->6 MB, 13 MB goal, 12 P
- gc 1407 @6.073s 11%: 0.052+1.8+0.20 ms clock, 0.62+1.5/2.2/0+2.4 ms cpu, 8->14->8 MB, 13 MB goal, 12 P
上面展示了如何使用GODEBUG變量生成GC trace。同時顯示了正在運行的Go應(yīng)用程序生成的3條trace信息。
下面對GC trace中的每個值的含義進行的分解。
- gc 1405 @6.068s 11%: 0.058+1.2+0.083 ms clock, 0.70+2.5/1.5/0+0.99 ms cpu, 7->11->6 MB, 10 MB goal, 12 P
- // General
- gc 1404 : The 1404 GC run since the program started
- @6.068s : Six seconds since the program started
- 11% : Eleven percent of the available CPU so far has been spent in GC
- // Wall-Clock
- 0.058ms : STW : Mark Start - Write Barrier on
- 1.2ms : Concurrent : Marking
- 0.083ms : STW : Mark Termination - Write Barrier off and clean up
- // CPU Time
- 0.70ms : STW : Mark Start
- 2.5ms : Concurrent : Mark - Assist Time (GC performed in line with allocation)
- 1.5ms : Concurrent : Mark - Background GC time
- 0ms : Concurrent : Mark - Idle GC time
- 0.99ms : STW : Mark Term
- // Memory
- 7MB : Heap memory in-use before the Marking started
- 11MB : Heap memory in-use after the Marking finished
- 6MB : Heap memory marked as live after the Marking finished
- 10MB : Collection goal for heap memory in-use after Marking finished
- // Threads
- 12P : Number of logical processors or threads used to run Goroutines
上面顯示了GC trace(1405)。最終將涉及其中大部分內(nèi)容,但是現(xiàn)在只關(guān)注1045 GC trace的內(nèi)存部分。
- // Memory7MB : Heap memory in-use before the Marking started
- 11MB : Heap memory in-use after the Marking finished
- 6MB : Heap memory marked as live after the Marking finished
- 10MB : Collection goal for heap memory in-use after Marking finished
通過此GC trace可以看出,在標記工作開始之前,使用中的堆內(nèi)存量為7MB。標記工作完成后,使用中的堆內(nèi)存量達到11MB。這意味著在收集過程中有4MB新分配內(nèi)存。標記工作完成后活動堆內(nèi)存量為6MB。這意味著在下一次垃圾收集啟動前,應(yīng)用程序可以將堆內(nèi)存增加到12MB。
你可以看到垃圾收集器Mark的目標和實際值之間有1MB差異。標記工作完成后正在使用的堆內(nèi)存量為11MB而不是10MB。因為Mark目標是根據(jù)當前正在使用的堆內(nèi)存量等信息計算出來的。應(yīng)用程序的改變導(dǎo)致在Marking之后使用更多堆內(nèi)存。
如果查看下一個GC trace(1406),可以看到在2ms內(nèi)發(fā)生了很多變化。
- gc 1406 @6.070s 11%: 0.051+1.8+0.076 ms clock, 0.61+2.0/2.5/0+0.91 ms cpu, 8->11->6 MB, 13 MB goal, 12 P
- // Memory
- 8MB : Heap memory in-use before the Marking started
- 11MB : Heap memory in-use after the Marking finished
- 6MB : Heap memory marked as live after the Marking finished
- 13MB : Collection goal for heap memory in-use after Marking finished
這里顯示了本次垃圾收集在上一次垃圾收集開始后2ms(6.068s對6.070s)就開始,使用中的堆內(nèi)存達到8MB。由于應(yīng)用程序大量分配內(nèi)存,并且垃圾收集器希望減少此收集期間的因為Mark Assist導(dǎo)致的延遲,垃圾收集可能會提前。
還有兩點需要注意。這次垃圾收集器完成了目標。標記完成后正在使用的堆內(nèi)存量為11MB而不是13MB,少了2 MB。標記完成后活動堆內(nèi)存依然是6MB。
可以通過添加gcpacertrace = 1從GC trace中獲取更多詳細信息。這會導(dǎo)致垃圾收集器打印有關(guān)并發(fā)起搏器內(nèi)部狀態(tài)的信息。
- $ export GODEBUG=gctrace=1,gcpacertrace=1 ./app
- Sample output:
- gc 5 @0.071s 0%: 0.018+0.46+0.071 ms clock, 0.14+0/0.38/0.14+0.56 ms cpu, 29->29->29 MB, 30 MB goal, 8 P
- pacer: sweep done at heap size 29MB; allocated 0MB of spans; swept 3752 pages at +6.183550e-004 pages/byte
- pacer: assist ratio=+1.232155e+000 (scan 1 MB in 70->71 MB) workers=2+0
- pacer: H_m_prev=30488736 h_t=+2.334071e-001 H_T=37605024 h_a=+1.409842e+000 H_a=73473040 h_g=+1.000000e+000 H_g=60977472 u_a=+2.500000e-001 u_g=+2.500000e-001 W_a=308200 goalΔ=+7.665929e-001 actualΔ=+1.176435e+000 u_a/u_g=+1.000000e+000
運行GC trace可以告訴你很多關(guān)于應(yīng)用程序的運行狀況和收集器速度的信息。收集器運行的速度在收集過程中起著重要作用。
起博
垃圾收集器使用調(diào)步算法,該算法用于確定何時開始垃圾收集。該算法依賴于運行中的應(yīng)用程序的信息以及應(yīng)用程序分配內(nèi)存的壓力。壓力即應(yīng)用程序在給定時間內(nèi)分配堆內(nèi)存的速度。正是壓力決定了垃圾回收器的速度。
在垃圾收集器開始收集之前,它會計算預(yù)期完成垃圾收集的時間。一旦垃圾收集器開始運行,會影響正在運行的應(yīng)用程序,造成延遲,拖慢用程序。每次收集都會增加應(yīng)用程序的整體延遲。降低收集器的啟動頻率并非提高性能的方法。
可以將GC百分比值更改為大于100的值。這將增加在下一次收集啟動之前可以分配的堆內(nèi)存量。也導(dǎo)致垃圾收集時間更長。
上圖顯示了更改GC百分比如何允許分配的堆內(nèi)存。 可以直觀地了解使用更多對內(nèi)存如何降低垃圾收集的速度。
降低收集器的啟動頻率無法幫助垃圾收集器更快完成收集工作。降低頻率會導(dǎo)致垃圾收集器在收集期間完成更多的工作(譯者注:因為分配了更多的內(nèi)存)。 可以通過減少新分配對象數(shù)量來幫助垃圾收集器更快完成收集工作。
注意:這個做法同時可以用盡可能小的堆來實現(xiàn)所需的吞吐量。 在云環(huán)境中運行時,最小化堆內(nèi)存等資源的使用非常重要。
上圖顯示了關(guān)于正在運行的應(yīng)用程序的一些統(tǒng)計信息。藍色版本顯示的是沒有優(yōu)化的應(yīng)用程序的統(tǒng)計信息。綠色版本是在去掉4.48GB的非生產(chǎn)性內(nèi)存分配后的統(tǒng)計數(shù)據(jù)。
查看兩個版本的平均收集速度(2.08ms vs 1.96ms),都約為2.0毫秒。這兩個版本之間的根本變化在于每次垃圾收集時候的吞吐量。從3.98提高到了7.13個請求。吞吐量增加79.1%。垃圾收集的時間并沒有隨著內(nèi)存分配的減少而減慢,而是保持不變。性能提升來自于每次垃圾收集期間,其他go routine可以完成更多工作。
調(diào)整垃圾收集的起博速度以推遲延遲成本并非提高應(yīng)用程序性能的方式。
收集器延遲成本
每次垃圾收集會造成兩種類型的延遲。 首先是竊取CPU容量。 這種被盜CPU容量的影響意味著應(yīng)用程序在垃圾收集過程中沒有全速運行。應(yīng)用程序Goroutines現(xiàn)在與垃圾收集器的Goroutines共享P或完成Mark Assist。
上圖顯示了應(yīng)用程序使用75%的CPU工作。 這是因為收集器本身就有專用的P1。
上圖顯示了應(yīng)用程序(通常只有幾微秒)只能將其CPU容量的一半用于應(yīng)用程序工作。 因為P3上的goroutine正在執(zhí)行Mark Assist,而且垃圾收集器已經(jīng)將P1占為己有。
第二種延遲是收集期間發(fā)生的STW延遲。 STW期間沒有應(yīng)用程序Goroutines執(zhí)行任何應(yīng)用程序。 該應(yīng)用程序基本上已停止。
上圖顯示了所有Goroutines都停止的STW延遲。 每次垃圾收集都會發(fā)生兩次。 如果應(yīng)用程序正常運行,則垃圾收集器能夠?qū)⒋蟛糠掷占目係TW時間保持在100微秒或以下。
現(xiàn)在了解了垃圾收集器的不同階段,內(nèi)存的大小,調(diào)整的工作方式以及垃圾收集器對正在運行的應(yīng)用程序造成的不同延遲。 有了這些知識,最終可以回答如何調(diào)優(yōu)的問題。
調(diào)優(yōu)
減少堆內(nèi)存的壓力是最好的優(yōu)化方式。 壓力可以定義為應(yīng)用程序在給定時間內(nèi)分配堆內(nèi)存的速度。 當堆內(nèi)存壓力減小時,垃圾收集器造成的影響會減少。減少GC延遲的方法是從應(yīng)用程序中識別并去掉不必要的內(nèi)存分配。
以下做法可以幫助垃圾收集器:
- 盡可能保持最小的堆。
- 最佳的一致的起博頻率。
- 保持在每次收集的目標之內(nèi)。
- 最小化每次垃圾收集的STW和Mark Assist的持續(xù)時間。
所有這些都有助于減少垃圾回收造成延遲,也將提高應(yīng)用程序的性能和吞吐量。 垃圾收集的頻率與此無關(guān)。
了解工作量意味著確保使用合理數(shù)量的goroutine來完成工作。 CPU瓶頸與IO瓶頸的工作負載不同,需要不同的工程決策,可以參考本文。https://www.ardanlabs.com/blog/2018/12/scheduling-in-go-part3.html
了解數(shù)據(jù)意味著了解虛要解決的問題。 數(shù)據(jù)語義一致性是維護數(shù)據(jù)完整性的關(guān)鍵部分,并允允許你決定在堆上還是棧上分配內(nèi)存。https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html
結(jié)論
對Go語言運行時來說重要的是要認識到有效的內(nèi)存分配(幫助應(yīng)用程序的分配)和那些沒有無效的內(nèi)存分配(那些損害應(yīng)用程序)之間的差異。 然后就只能信任垃圾收集器可以高效的運行。
擁有垃圾收集器是一個很好的權(quán)衡。 雖然有垃圾收集的成本,但是卻沒有內(nèi)存管理的負擔(dān)。 Go語言同時兼顧了開發(fā)和運行效率。 垃圾收集器是實現(xiàn)這一目標的重要組成部分。
原文地址:
https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html
參考資料:
https://github.com/golang/go/blob/release-branch.go1.5/src/runtime/mgc.go
https://github.com/golang/proposal/blob/master/design/17505-concurrent-rescan.md
https://github.com/golang/proposal/blob/master/design/17503-eliminate-rescan.md
https://blog.golang.org/ismmkeynote
https://www.youtube.com/watch?v=aiv1JOfMjm0&t=1208s