go源码剖析笔记

环境

go version #go version go1.10.4 linux/amd64
lsb_release -d #Description:    Ubuntu 18.04.1 LTS
gdb --version #GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git

引导

测试代码 test.go

package main
func main() {
    println("hello, world");
}
go build -gcflags "-N -l" -o test test.go
gdb test
(gdb) info files
Symbols from "/mnt/c/Program Files/cmder/test".
Local exec file:
        `/mnt/c/Program Files/cmder/test', file type elf64-x86-64.
        Entry point: 0x4477c0
        0x0000000000401000 - 0x000000000044c213 is .text
        0x000000000044d000 - 0x00000000004757a3 is .rodata
        0x00000000004758e0 - 0x0000000000475f80 is .typelink
        0x0000000000475f80 - 0x0000000000475f88 is .itablink
        0x0000000000475f88 - 0x0000000000475f88 is .gosymtab
        0x0000000000475fa0 - 0x00000000004a3630 is .gopclntab
        0x00000000004a4000 - 0x00000000004a4a08 is .noptrdata
        0x00000000004a4a20 - 0x00000000004a65b0 is .data
        0x00000000004a65c0 - 0x00000000004c2888 is .bss
        0x00000000004c28a0 - 0x00000000004c4e58 is .noptrbss
        0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid
(gdb) b *0x4477c0
Breakpoint 1 at 0x4477c0: file /usr/lib/go-1.10/src/runtime/rt0_linux_amd64.s, line 8.

版本对应的汇编有变化,没有明显的main,但是入口肯定是_rt0_amd64

#include "textflag.h"

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
        JMP     _rt0_amd64(SB)

TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
        JMP     _rt0_amd64_lib(SB)
        
        
  
(gdb) b _rt0_amd64
Breakpoint 2 at 0x444100: file /usr/lib/go-1.10/src/runtime/asm_amd64.s, line 15.

对应汇编是书里的runtime.rt0_go

TEXT _rt0_amd64(SB),NOSPLIT,$-8
        MOVQ    0(SP), DI       // argc
        LEAQ    8(SP), SI       // argv
        JMP     runtime·rt0_go(SB)
b runtime.rt0_go
Breakpoint 3 at 0x444110: file /usr/lib/go-1.10/src/runtime/asm_amd64.s, line 89.
       ;前面有很多对于汇编指令cpu类型的判断,参数入栈等等
       // create a new goroutine to start program
        MOVQ    $runtime·mainPC(SB), AX                // entry
        PUSHQ   AX
        PUSHQ   $0                      // arg size
        CALL    runtime·newproc(SB)
        POPQ    AX
        POPQ    AX

        // start this M
        CALL    runtime·mstart(SB)

        MOVL    $0xf1, 0xf1  // crash
        RET

DATA    runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL   runtime·mainPC(SB),RODATA,$8
b runtime.schedinit
Breakpoint 6 at 0x423a60: file /usr/lib/go-1.10/src/runtime/proc.go, line 477.
b runtime.main
Breakpoint 4 at 0x4228b0: file /usr/lib/go-1.10/src/runtime/proc.go, line 109.

schedinit 入口

// The bootstrap sequence is:
//
//      call osinit
//      call schedinit
//      make & queue new G
//      call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
        // raceinit must be the first call to race detector.
        // In particular, it must be done before mallocinit below calls racemapshadow.
        _g_ := getg()
        if raceenabled {
                _g_.racectx, raceprocctx0 = raceinit()
        }

        sched.maxmcount = 10000

        tracebackinit()
        moduledataverify()
        stackinit()
        mallocinit()
        mcommoninit(_g_.m)
        alginit()       // maps must not be used before this call
        modulesinit()   // provides activeModules
        typelinksinit() // uses maps, activeModules
        itabsinit()     // uses activeModules
        
        msigsave(_g_.m)
        initSigmask = _g_.m.sigmask

        goargs()
        goenvs()
        //处理GODEBUG GOTRACEBACK宏
        parsedebugvars()
        //垃圾回收器初始化
        gcinit()

        sched.lastpoll = uint64(nanotime())
        //通过CPU core和GOMAXPROCS确定P数量
        procs := ncpu
        if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
                procs = n
        }
        // 调整P数量
        if procresize(procs) != nil {
                throw("unknown runnable goroutine during bootstrap")
        }

        // For cgocheck > 1, we turn on the write barrier at all times
        // and check all pointer writes. We can't do this until after
        // procresize because the write barrier needs a P.
        if debug.cgocheck > 1 {
                writeBarrier.cgo = true
                writeBarrier.enabled = true
                for _, p := range allp {
                        p.wbBuf.reset()
                }
        }


下一步是runtime.main

// The main goroutine.
func main() {
        g := getg()

        // Racectx of m0->g0 is used only as the parent of the main goroutine.
        // It must not be used for anything else.
        g.m.g0.racectx = 0

        // Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
        // Using decimal instead of binary GB and MB because
        // they look nicer in the stack overflow failure message.
        if sys.PtrSize == 8 {
                maxstacksize = 1000000000
        } else {
                maxstacksize = 250000000
        }

        // Allow newproc to start new Ms.
        //启动系统后台监控/定期垃圾回收,并发任务调度相关
        mainStarted = true
        systemstack(func() {
                newm(sysmon, nil)
        })

        // Lock the main goroutine onto this, the main OS thread,
        // during initialization. Most programs won't care, but a few
        // do require certain calls to be made by the main thread.
        // Those can arrange for main.main to run in the main thread
        // by calling runtime.LockOSThread during initialization
        // to preserve the lock.
        lockOSThread()

        if g.m != &m0 {
                throw("runtime.main not on m0")
        }

        runtime_init() // must be before defer
        if nanotime() == 0 {
                throw("nanotime returning zero")
        }

        // Defer unlock so that runtime.Goexit during init does the unlock too.
        needUnlock := true
        defer func() {
                if needUnlock {
                        unlockOSThread()
                }
        }()
            // Record when the world started. Must be after runtime_init
        // because nanotime on some platforms depends on startNano.
        runtimeInitTime = nanotime()

        gcenable()

        main_init_done = make(chan bool)
        if iscgo {
                if _cgo_thread_start == nil {
                        throw("_cgo_thread_start missing")
                }
                if GOOS != "windows" {
                        if _cgo_setenv == nil {
                                throw("_cgo_setenv missing")
                        }
                        if _cgo_unsetenv == nil {
                                throw("_cgo_unsetenv missing")
                        }
                }
                if _cgo_notify_runtime_init_done == nil {
                        throw("_cgo_notify_runtime_init_done missing")
                }
                // Start the template thread in case we enter Go from
                // a C-created thread and need to create a new thread.
                startTemplateThread()
                cgocall(_cgo_notify_runtime_init_done, nil)
        }

        fn := main_init // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
        fn()
        close(main_init_done)

        needUnlock = false
        unlockOSThread()

        if isarchive || islibrary {
                // A program compiled with -buildmode=c-archive or c-shared
                // has a main, but it is not executed.
                return
        }
        fn = main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
        fn()
        if raceenabled {
                racefini()
        }
        // Make racy client program work: if panicking on
        // another goroutine at the same time as main returns,
        // let the other goroutine finish printing the panic trace.
        // Once it does, it will exit. See issues 3934 and 20018.
        if atomic.Load(&runningPanicDefers) != 0 {
                // Running deferred functions should not take long.
                for c := 0; c < 1000; c++ {
                        if atomic.Load(&runningPanicDefers) == 0 {
                                break
                        }
                        Gosched()
                }
        }
        if atomic.Load(&panicking) != 0 {
                gopark(nil, nil, "panicwait", traceEvGoStop, 1)
        }

        exit(0)
        //? 这啥
        for {
                var x *int32
                *x = 0
        }

一个复杂示例

//cat lib/sum.go
package lib
func init() {
    println("sum.init")
}

func Sum(x ...int) int {
    n  := 0
    for _, i := range x{
        n += i
    }
    return n
}
//cat test.go
package main
import (
    "./lib"
)
func init() {
    println("test.init")
}

func test() {
    println(lib.Sum(1,2,3))
}

//cat main.go
package main

import (
        _ "net/http"
)

func init() {
    println("main.init.2")
}

func main() {
    test()
}

func init() {
    println("main.init.1")
}

执行结果

go build -gcflags "-N -l" -o test
./test
sum.init
main.init.2
main.init.1
test.init
6

查看反汇编

;go tool objdump -s "runtime\.init\b" test
TEXT runtime.init.0(SB) /usr/lib/go-1.10/src/runtime/cpuflags_amd64.go
TEXT runtime.init.1(SB) /usr/lib/go-1.10/src/runtime/mgcwork.go
  mgcwork.go:25         0x420860                c3                      RET
TEXT runtime.init.2(SB) /usr/lib/go-1.10/src/runtime/mstats.go
  mstats.go:438         0x4260d0                64488b0c25f8ffffff      MOVQ 
TEXT runtime.init.3(SB) /usr/lib/go-1.10/src/runtime/panic.go
TEXT runtime.init.4(SB) /usr/lib/go-1.10/src/runtime/proc.go
TEXT runtime.init.5(SB) /usr/lib/go-1.10/src/runtime/signal_unix.go
  signal_unix.go:64     0x43e450                c3                      RET
TEXT runtime.init(SB) <autogenerated>


;go tool objdump -s "main\.init\b" test
TEXT main.init.0(SB) /mnt/c/Program Files/cmder/main.go
TEXT main.init.1(SB) /mnt/c/Program Files/cmder/main.go
TEXT main.init.2(SB) /mnt/c/Program Files/cmder/test.go
TEXT main.init(SB) <autogenerated>
 <autogenerated>:1     0x5e31ec                e81f63ffff              CALL net/http.init(SB)

  <autogenerated>:1     0x5e31f1                e83afdffff              CALL _/mnt/c/Program_Files/cmder/lib.init(SB)
  <autogenerated>:1     0x5e31f6                e895fdffff              CALL main.init.0(SB)

  <autogenerated>:1     0x5e31fb                e820feffff              CALL main.init.1(SB)

  <autogenerated>:1     0x5e3200                e87bfeffff              CALL main.init.2(SB)

  <autogenerated>:1     0x5e3205                c605822a1f0002          MOVB $0x2, main.initdone.(SB)

  <autogenerated>:1     0x5e320c                488b2c24                MOVQ 0(SP), BP

  <autogenerated>:1     0x5e3210                4883c408                ADDQ $0x8, SP

  <autogenerated>:1     0x5e3214                c3                      RET

  <autogenerated>:1     0x5e3215                e80600e7ff              CALL runtime.morestack_noctxt(SB)

  <autogenerated>:1     0x5e321a                eb84                    JMP main.init(SB)

结论

所有init都会在同一个goroutine执行

所有init函数结束后才会执行main.main

内存分配

基本策略

  • 每次从操作系统申请一大块内存,减少系统调用
  • 内存分配器
    • 大块内存预先切成小块构成链表
    • 分配就从链表里提取一块
    • 回收旧放回链表
    • 空闲过多会归还给系统降低整体开销

内存块

  • span page 大块内存
  • object切分span多个小块
  • 哦,抄的tcmalloc

初始化动作

三个数组组成内存管理结构

  • spans,管理span的,按页对应,地址按页对齐能快速定位(?这里的原理不太清楚,我对页这些东西计算一直处于一知半解水平)
  • bitmap 为每个对象提供4bit标记为,保存指针,GC标记
  • arena 申请内存,用户可分配上限

arena和spans bitmap存在映射关系,三者可以按需同步线性扩张

都用mheap维护,在mallocinit里初始化

来个示例

//test.go
package main

import(
    "fmt"
    "os"
    "github.com/shirou/gosutil/process"
)

var ps *process.Process

func mem(n int) {
    if ps == nil {
            p, err := process.NewProcess(int32(os.Getpid()))
                if err != nil {
                    panic(err)
                }

                ps = p
        }

        mem, _ := ps.MemoryInfoEx()
        fmt.Printf("%d, VMS:%d MB, RSS:%d MB\n", n, mem,.VMS>>20, mem.RSS>>20)
}

func main(){
    mem(1)
        data : new([10][1024*1024]byte)
    mem(2)

    for i := range data {
            for x, n := 0, len(data[i]); x<n; x++ {
                    data[i][x] = 1
                }
                mem(3)
        }
}

分配

不要以为new一定会分配在堆上,随着优化内联

package main
import ()
func test() *int {
    x := new(int)
    *x = 0xAABB
    return x
}
func main() {
    println(*test())
}
go build -gcflags "-l" -o test test.go
go tool objdump -s "main\.test" test
TEXT main.test(SB) /mnt/c/Program Files/cmder/test.go
  test.go:4             0x44c150                64488b0c25f8ffffff      MOVQ FS:0xfffffff8, CX
  test.go:4             0x44c159                483b6110                CMPQ 0x10(CX), SP
  test.go:4             0x44c15d                7639                    JBE 0x44c198
  test.go:4             0x44c15f                4883ec18                SUBQ $0x18, SP
  test.go:4             0x44c163                48896c2410              MOVQ BP, 0x10(SP)
  test.go:4             0x44c168                488d6c2410              LEAQ 0x10(SP), BP
  test.go:5             0x44c16d                488d05acac0000          LEAQ 0xacac(IP), AX
  test.go:5             0x44c174                48890424                MOVQ AX, 0(SP)
  test.go:5             0x44c178                e8a3effbff              CALL runtime.newobject(SB)
  test.go:5             0x44c17d                488b442408              MOVQ 0x8(SP), AX
  test.go:6             0x44c182                48c700bbaa0000          MOVQ $0xaabb, 0(AX)
  test.go:7             0x44c189                4889442420              MOVQ AX, 0x20(SP)
  test.go:7             0x44c18e                488b6c2410              MOVQ 0x10(SP), BP
  test.go:7             0x44c193                4883c418                ADDQ $0x18, SP
  test.go:7             0x44c197                c3                      RET
  test.go:4             0x44c198                e8d383ffff              CALL runtime.morestack_noctxt(SB)
  test.go:4             0x44c19d                ebb1                    JMP main.test(SB)
  


go build -o test test.go
go tool objdump -s "main\.main" test
TEXT main.main(SB) /mnt/c/Program Files/cmder/test.go
  test.go:10            0x44c150                64488b0c25f8ffffff      MOVQ FS:0xfffffff8, CX
  test.go:10            0x44c159                483b6110                CMPQ 0x10(CX), SP
  test.go:10            0x44c15d                7634                    JBE 0x44c193
  test.go:10            0x44c15f                4883ec10                SUBQ $0x10, SP
  test.go:10            0x44c163                48896c2408              MOVQ BP, 0x8(SP)
  test.go:10            0x44c168                488d6c2408              LEAQ 0x8(SP), BP
  test.go:11            0x44c16d                e88e59fdff              CALL runtime.printlock(SB)
  test.go:11            0x44c172                48c70424bbaa0000        MOVQ $0xaabb, 0(SP)
  test.go:11            0x44c17a                e80161fdff              CALL runtime.printint(SB)
  test.go:11            0x44c17f                e80c5cfdff              CALL runtime.printnl(SB)
  test.go:11            0x44c184                e8f759fdff              CALL runtime.printunlock(SB)
  test.go:12            0x44c189                488b6c2408              MOVQ 0x8(SP), BP
  test.go:12            0x44c18e                4883c410                ADDQ $0x10, SP
  test.go:12            0x44c192                c3                      RET
  test.go:10            0x44c193                e8d883ffff              CALL runtime.morestack_noctxt(SB)
  test.go:10            0x44c198                ebb6                    JMP main.main(SB)

逃逸分析-gcflag “-m”

分配思路 malloc.go

  • 大对象heap
  • 小对象cache.alloc[sizeclass].freelist object
  • 微小对象使用cache.tiny object

回收

回收以span为单位

释放

sysmon监控任务来搞

具体释放是madvie(v, n, _MADV_DONTNEED) 系统来决定。如果物理内存资源充足,就不会回收避免无谓的损耗,不过再次使用肯定会pagefault然后分配新的内存

垃圾回收

缩短STW时间

抑制堆增长 充分利用CPU资源

  • 三色标记和写屏障
    • 所有都是白色
    • 扫描出所有可达对象,标记成灰色,放出待处理队列
    • 队列提取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色
    • 写屏障监视对象内崔修改,重新标色或放回队列

gcController控制

辅助回收,避免分配速度大于后台标记导致的堆恶性扩张

ref


Read More

八月待读 need review

公司不让访问外网了呜呜

https://support.hypernode.com/en/troubleshooting/performance/how-to-debug-out-of-memory-oom-events

https://briancallahan.net/blog/20200816.html

https://lobste.rs/s/2vj4sb/file_handling_unix_tips_traps_outright

https://danluu.com/file-consistency/

https://rachelbythebay.com/w/2020/08/11/files/

https://robertovitillo.com/what-every-developer-should-know-about-database-consistency/

http://brooker.co.za/blog/2020/05/25/reading.html

https://codahale.com/work-is-work/

https://bartoszmilewski.com/2020/08/11/benign-data-races-considered-harmful/

https://www.fluentcpp.com/2020/06/12/a-generic-component-for-out-of-line-lambdas/

http://brooker.co.za/blog/2020/03/22/rust.html

https://briancallahan.net/blog/20200812.html

https://blog.kevinjahns.de/are-crdts-suitable-for-shared-editing/

https://zhuanlan.zhihu.com/p/108968057

https://www.cockroachlabs.com/blog/cockroachdb-sigmod-2020/?utm_campaign=cooperpress-2020-Q2&utm_source=dbweekly&utm_medium=newsletter&utm_content=dbweekly-primary-sigmod

https://ketanbhatt.com/db-concurrency-defects/

Read More

(译)Data Structures Part 1 Bulk Data

作者把数据结构抽象了,不按照传统的什么树跳表堆什么的,根据用途来分类

  • Bulk Data,也就是保存大量对象
  • 弱引用/handle 对于Bulk Data的引用,即使对象没了也不会访问崩溃
  • indices索引,访问特定Bulk Data的索引下表
  • 数组的数组,保存一大堆Bulk Data的对象

其实这种和之前提到的index-handle有点像

这里介绍Bulk Data

作者也没有好的术语来概括他们,大概就是一组数据

  • 游戏中所有的子弹
  • 游戏中所有的树
  • 游戏中所有的硬币

更抽象一点

  • 所有游戏中的项目
  • 所有游戏中的网格物品
  • 所有游戏中的声音/音效

并且每种类型都有很多属性来判断,对于音效

  • 所有能被播放的声音资源
  • 当前播放的声音
  • 所有能加到音轨上的音效(淡入淡出,摩擦声等等)

对于bulk data资源,我们假定

  • 顺序不重要,当成set
  • 每种类型都是POD类型,可以memcpy

可能某些场景顺序也有要求,比如需要渲染的对象,需要从头到尾渲染一遍

作者推荐把需要顺序的捞出来主动排序一下,而不是用个能排序的容器来保存,基数排序O(n) 也能接受(本来数据规模也不大)

至于POD类型,这里假定所有对象都是固定大小的POD,至于有的可能是链表结构这个放到后面数组的数组在讨论

这里还以声音资源来举例

typedef struct {
    resource_t * resource; // 资源 引用
    uint64_t bytes;        // 资源大小
    uint64_t format;       // 资源的格式判断位
} sound_resource_t;

typedef struct {
    sound_resource_t *resource; // 正在播放的资源 引用
    uint64_t samples_played;    // 播放了的个数
    float volume;               // 音量
} playing_sound_t;

typedef struct {
    playing_sound_t *sound;     // 渐入的资源 引用
    float fade_from;            // 音量
    float fade_to;              // 音量
    double fade_from_ts;        // 什么时候开始渐入
    double fade_to_ts;          // 什么时候结束渐入
} playing_fade_t;

我们对于保存bulk data有这么几个要求

  • 增删对象要快
  • 对象的存储布局要cache-friendly,能更快的遍历
  • 支持引用
  • allocator-friendly 对于分配器友好?不如说分配器要牛逼一点

最简单的实现bulk data的方法就是用一个静态数组或者一个vector

#define MAX_PLAYING_SOUNDS 1024
uint32_t num_playing_sounds;
playing_sound_t playing_sounds[MAX_PLAYING_SOUNDS];

// C++ vector
std::vector<playing_sound_t> playing_sounds;

如果你知道对象的个数用静态数组非常见到粗暴,如果不确定,可能浪费或者不够用

使用std::vector要注意一些问题

  • DEBUG模式下 VS的std::vector非常慢,要注意设置 _ITERATOR_DEBUG_LEVEL=0
  • std::vector用构造析构创建删除对象,在某些场景要比memcpy慢(应该指的搬迁)
  • std::vector更难自省??要比自己实现一个更难观测(stretchy弹性)

另外两者都不支持对象引用???

删除策略

如果a[i]被删,几种处理手段

  • 搬迁后面的节点,覆盖空的slot 太浪费时间,除非你要保证顺序(尤其是erase)

  • 直接搬迁最后面的,swap-and-pop

    std::swap(a[i], a[a.size() - 1]);
    a.pop_back();
    

    直接从后面分配,无脑

    • 更紧凑遍历更快
    • index会变,就会类似hashtable重整,属性全丢了
  • 留着这个洞,后面直接分配这个 freelist 需要在用slot前检查

    • index信息没变
    • 遍历有洞会慢

作者建议各取所需,除非对遍历有强需求,否则第三种很简单,idindex信息也都在,使用更方便

可能最终维护框架长这个样子

// The objects that we want to store:
typedef struct {...} object_t;

// An item in the free list points to the next one.
typedef struct {
    uint32_t next_free;
} freelist_item_t;

// Each item holds either the object data or the free list pointer.
typedef union {
    object_t;
    freelist_item_t;
} item_t;

typedef struct {
    std::vector<item_t> items;
} bulk_data_t;

void delete_item(bulk_data_t *bd, uint32_t i) {
    // Add to the freelist, which is stored in slot 0.
    bd->items[i].next = bd->items[0].next;
    bd->items[0].next = i;
}

uint32_t allocate_slot(bulk_data_t *bd) {
    const uint32_t slot = bd->items[0].next;
    bd->items[0].next = bd->items[slot].next;
    // If the freelist is empty, slot will be 0, because the header
    // item will point to itself.
    if (slot) return slot;
    bd->items.resize(bd->items.size() + 1);
    return bd->items.size() - 1;
}

weak pointers

直接用索引信息就可以了

加上版本信息就可以了

typedef struct {
    uint32_t id;         // index
    uint32_t generation; // 引用计数类似物,版本号?
} weak_pointer_t;


typedef struct {
    uint32_t generation;
    union {
        object_t;
        freelist_item_t;
    };
} item_t;

分配策略

std::vector本身有内存放大问题,如果满了就会扩容搬迁,这导致了一些问题

  • 扩容造成的复制开销很大,可能造成延迟,这也是GC在游戏中会造成延迟的问题
  • 内存浪费

解决办法

结构体数组还是数组结构体?

数组结构体

  • 代码复杂
  • 分配器压力,分配被迫分散到不同的数组,而不是分配一个大的
  • 字段分散,得有index信息分别访问
  • 增加了计算量
  • cache不友好 但是紧凑起来可以加速simd
  • 删除不友好

最终结论 bulk data保存最终方案

There are advantages and drawbacks to everything, but my default recommendation for storing bulk data for a new system would be:

An array of structures, with “holes” and permanent pointers, either allocated as one single large VM reservation (if possible) or as an array of fixed size blocks (of 16 K or whatever is a good fit for your data).

And for the cases where you need really fast number crunching over the data:

A structure of arrays of tightly packed objects, grouped 8 at a time for SIMD processing and allocated as one single large VM reservation, or as an array of fixed-size blocks.

结构体数组 方便管理

数组结构体,可以用simd加速,但是不好管理

ref

  • 我是看lobste上的推荐看的这篇文章https://ourmachinery.com/post/data-structures-part-1-bulk-data/
  • 后面的系列
    • https://ourmachinery.com/post/data-structures-part-2-indices/
    • https://ourmachinery.com/post/data-structures-part-3-arrays-of-arrays/
  • 作者自己实现类似vector的容器 值得一看https://ourmachinery.com/post/minimalist-container-library-in-c-part-1/

Read More

(译)Handles are the better pointers

作者的一个经验,把指针转换成index-handles,也就是如何写不涉及分配和指针的程序

背景

  • 0.5到1百万行代码,智能指针管理内存
  • 万到十万以上的小对象,堆分配,智能指针管理

这种管理方式基本不会有segfault,有问题的代码也都能抓到问题根源

但是存在问题

  • dog-slow?遍地都是cache-miss
  • 内存碎片问题
  • fake memory leaks 内存指针占用的内存没有及时回收,看上去像是内存泄漏了,而这种问题通过内存检测工具也检查不出来

作者给的方法有一定借鉴意义(甚至对于那些GC语言,因为会面临同样的场景,大量小对象)

但是可能要要求创建销毁对象尽可能的集中,某些场景可能不可行,也就是面向数据

这里的使用场景是游戏场景,有大量小对象频繁创建删除

简要概括

  • 所有的内存管理集中成一个模块,用这个模块来管理分配
  • 把小对象分类,每个小对象有自己的数组指针-索引来管理
  • 创建对象,返回index-handle就可以了
  • 需要的话,就把handle强转指针来用,并且任何地方都不要保存指针

感觉这是个很常见的CRTP分配器套路,尽可能的复用内存,对于代理型应用,都会这么设计,作者是游戏型应用,也是有大量小对象大量的创建删除的。一个好的分配器能降低系统碎片且让cpu利用率高,不轻易cache miss –这些都影响延迟

下面作者说了针对他们这个场景的几点优势

  • 管理高效
  • cache friendly
    • 内存数据尽可能的热
  • 不会有很多的内存碎片

全面用这个index-handle替换指针的优势

  • 避免直接访问内存,安全
  • 只有分配器组件知道这个内存分配的细节和指针信息,别人只能通过index拿到 (其实index也可以隐藏起来)
  • 内存增长更安全,会尽可能复用,不会激增

但是这要求应用本身不能乱用指针,因为引用的对象随时会变

这个东西和普通的CRTP 内存池还不太一样。代码我还没有读。有些作者的设计点我没有领会完整。后面有时间会看看

ref

  • https://floooh.github.io/2018/06/17/handles-vs-pointers.html
  • 作者的封装库 https://github.com/floooh/sokol#sokol_gfxh
  • 背景文档 https://floooh.github.io/2018/06/02/one-year-of-c.html 也值得一看
  • 我是看lobste上的推荐看的这篇文章,还有https://ourmachinery.com/post/data-structures-part-1-bulk-data/ 后面也要看看

Read More

Seastar资料整理以及介绍

  • https://forrestsu.github.io/posts/archi-seastar/seastar-future-promise/ future promise原理

future promise穿成链表,包装task回调

  • https://zhuanlan.zhihu.com/p/113119124

介绍了主要特点 核间通信,以及一些优化上的优点

用到的无锁队列

使用的应用

cpv-framework: A web framework written in c++ based on seastar framework

redpanda: A Kafka replacement for mission critical systems

Scylla: A fast and reliable NoSQL data store compatible with Cassandra and DynamoDB

smf: rpc框架,用的fbs

seastar侵染性太强,沾上就得用


Read More

folly资料整理以及介绍以及一些看到的用法

  • IOThreadPool :每个线程中包含一个 EventLoop ,即一个 epoll 的处理器。添加任务时,添加到通知队列, epoll 循环可以收到通知,处理任务。额外还可以添加IO事件回调。
  • CPUThreadPool :和 IOThreadPool 相比,它简单一些,添加任务时,添加到阻塞队列,以信号的形式通知线程,空闲线程执行任务。
  • ConcurrentHashMap 基于hazard pointer https://zhuanlan.zhihu.com/p/104308755
  • 为啥有concurrenthashmap还要又个atomichashmap? 实现原理差不多 https://github.com/facebook/folly/blob/master/folly/concurrency/ConcurrentHashMap.h
  • iobuf 参考的tcp协议栈里的mbuf https://github.com/facebook/folly/blob/master/folly/io/IOBuf.h

    • 这个东西我看redpanda也用了。要省network 浪费, iobuf避免不了

wangle介绍

https://my.oschina.net/fileoptions/blog/881909

  • Synchronized lock guard和metux的封装。省事儿了
  folly::Synchronized<Meta> meta_;
  meta_.withRLock([&](auto& cluster_meta) {
 .......
  });
  • Singleton

一个组合使用的例子

namespace {
struct PrivateTag {};
}  // namespace
namespace my {

class Scheduler {
 private:
  std::unique_ptr<folly::CPUThreadPoolExecutor> executor_;

 public:
  static std::shared_ptr<Scheduler> GetInstance();
  Scheduler() {
  executor_ = std::make_unique<folly::CPUThreadPoolExecutor>(
      4, std::make_shared<folly::NamedThreadFactory>("worker"));}
  ~Scheduler() { executor_->stop(); }
};

static folly::Singleton<Scheduler, PrivateTag> scheduler_singleton;
std::shared_ptr<Scheduler> Scheduler::GetInstance() { return the_singleton.try_get(); }
}

比较常规没啥说的

  • ThreadLocalPRNG

底层实现是thread local singleton,所以就规避了反复创建random device的问题

folly::ThreadLocalPRNG rng;
auto rand = folly::Random::rand32(min, max, rng);
  • folly::ReadMostlySharedPtr

读优化的shared ptr。不过用了这个就不能和普通shared ptr互通了

  • ThreadCachedInt

原理可以看这个https://blog.csdn.net/cjk_cynosure/article/details/109320285

思路就是thread_local 然后聚合一下。这样就避免了原子加减

之前也见过类似的思路,https://travisdowns.github.io/blog/2020/07/06/concurrency-costs.html

原子数组+线性探测,大量使用ThreadCachedInt

没啥说的

  • folly::IndexedMemPool

小对象池子 https://github.com/facebook/folly/blob/main/folly/IndexedMemPool.h

  • folly::AccessSpreader

暗示这个地址是这个线程独享的,降低竞争

  • hhwheeltimer

  • 场景:
    • 心跳检测
    • 游戏技能冷却
    • 倒计时
    • 其他需要延时处理的功能
  • 触发
    • 利用I/O多路复用系统调用的最后一个参数(超时时间),来触发检测定时器。
    • 利用timefd,将定时检测作为I/O多路复用当中的事件进行处理。
Read More

七月待读 need review

我发现越攒越多了这东西

https://medium.com/swlh/building-rest-api-backed-by-redis-ae8ff4818460

https://github.com/mhewedy-playground/RecipeAPI/blob/master/main.go

https://tech.marksblogg.com/omnisci-macos-macbookpro-mbp.html

https://medium.com/google-cloud/spanners-sql-story-79bda8bb632d

https://zhuanlan.zhihu.com/p/141891855

https://fauna.com/blog/demystifying-database-systems-part-4-isolation-levels-vs-consistency-levels

https://pingcap.com/blog/how-tidb-htap-makes-truly-hybrid-workloads-possible/

https://blog.yugabyte.com/why-distributed-sql-beats-polyglot-persistence-for-building-microservices/?utm_source=db_weekly&utm_medium=sponsored_link&utm_campaign=july172020

https://vladmihalcea.com/sql-left-join/

https://tiledb.com/blog/tiledb-closes-15m-series-a-for-industry-s-first-universal-data-engine-2020-07-14

https://ketanbhatt.com/db-concurrency-defects/

leveldb pdf 已经读过了。记录一下

https://yuerblog.cc/wp-content/uploads/leveldb%E5%AE%9E%E7%8E%B0%E8%A7%A3%E6%9E%90.pdf

C++ 中把 lambda 优雅地转化为函数指针 · Terark/terarkdb Wiki

https://github.com/Terark/terarkdb/wiki/C%EF%BC%8B%EF%BC%8B-%E4%B8%AD%E6%8A%8A-lambda-%E4%BC%98%E9%9B%85%E5%9C%B0%E8%BD%AC%E5%8C%96%E4%B8%BA%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88

Read More

python笔记以及遇到的坑

## pyyaml

  • pyyaml会有特殊的字符串转换,比如把yes转换成true,所以必须要把yes用引号括起来,或者定制yaml的构造函数,定制resolver,见参考链接的做法。

    • 修改意见:主动引号括起来。或者换名字。定制resolver杀鸡牛刀
  • pyyaml对于一些key会加引号,当初由于上层应用不是标准yaml库解析,不能解析引号,所以采用了ruamel.yaml这个库,设置指定的dumper可以让字段不带引号,但是这个库的实现有很大问题,建议别用,而且你能搜到该库作者的很多SO网站回答推广,真有你的啊

    • 不要用,出来混早晚要还的。千万别取巧
  • eventlet 0.25版本,会依赖dnspython库,这个库不能用2.0版本,接口不兼容

    • 解决方法 固定dnspython在1.16或者升级eventlet到0.25.2以上

xml

之前以为xml库难用,其实是我不理解xml

hdfs的配置文件格式xml ,我需要解析一个配置项,改值

  <property>
      <name>dfs.client.failover.max.attempts</name>
      <value>10</value>
      <description>
      if multiply namenodes are configured, it is the max retry times when the dfs client try to issue a RPC call. default is 75.
      </description>
  </property>
  <property>
      <name>dfs.ratelimiter.enabled</name>
      <value>false</value>
      <description>
      ratelimiter enabled or not.
      </description>
  </property>
  
  <property>
      <name>limiter.bandwidth</name>
      <value>62914560</value>
      <description>
      the limited highest bandwidth.
      </description>
  </property>
  
  <property>
      <name>limiter.iops</name>
      <value>60000</value>
      <description>
      the limited highest iops.
      </description>
  </property>
  

主要是限流的改动,全去掉

依据我之前使用json yaml的经验,都是kv型的,怎么也改不了value字段的值

部门高手写了这么段代码,真暴力

xml是树型的,都是子节点,也就是说上面匹配到了text,那下一个肯定是value字段。。。

我之前的考虑就是奔着value字段改的。笨比了。

  import sys
  import xml.etree.ElementTree as ET
  
  tree = ET.parse('hdfs-client.xml')
  root = tree.getroot()
  
  found = False
  for child in root.iter():
      if child.text == 'dfs.client.failover.max.attempts':
          found = True
          continue
      if found:
          child.text = "111"
          found = False
          break
  
  
  tree.write("hdfs-client.xml")
  
  • xml真是太让我头疼了

进程连接不上

  • 网络问题
  • 进程卡死

不要很自信的说网络问题。自己模块写的有问题进程卡死也是有可能的

Python核心语法

  • 共享引用 在原处更改

is == 区别

  • 小的整数和字符串缓存复用了 sys.getrefcount(1)

  • 函数
    • def 生成函数对象并复制给函数名 lambda直接返回函数
    • yield 返回一个结果对象,保存上下文
    • global 模块级变量 nonlocal 类似global,用于闭包
    • 闭包,有点像c++里的函数对象 python中类实现__call__(self)也行,书上说这种写法过于重型
  • 参数
    • 传参是引用还是传值 In-Place Change
    • 避免 主动传值[:],传tuple
    • 任一参数*pargs kargs pargs是元组 **kargs是字典,用来解包 引申-》Keyword-Only 用来指定元组和字典。单独使用做占位参数,则需要调用方保证指定所有入参(a=8这种)注意必须是最后一个参数
  • 再谈函数
    • map(functor, iterable-obj) std::for_each filter std::remove functools.reduce std::acc,
    • 迭代,next()语法糖
      • yield,生成器,for,range这些都是生成器函数
      • 生成器表达式
      • 可迭代对象
  • 模块
    • import from from 危害,模块冲突
    • import 决定将__name__ = ‘main‘解释成模块还是函数
    • 运算符重载
    • __str__
    • __repr__
    • 继承,真正意义上的接口。不会调用基类构造函数
from abc import abstractmethod
@abstractmethod

__getitem__ 买一送一

_iter__

__next__

__del__

__getaddr__ 属性钩子

__slot__

  • 装饰器与元类
    • 类做装饰器
    • 函数装饰类 元类
class tracer:
  def __init__(self, func):
    self.func = func;
  def __call__(self,*args):
    self.func(*args)
  • 异常
    • try except finally
    • raise assert
    • with as
      • with 环境管理协议 enter exit
      • 主动释放with内资源。包括多线程的锁条件变量之类dir

ref

  • https://stackoverflow.com/questions/34282703/yes-or-no-dumping-as-yes-or-no-in-pyyaml
  • https://github.com/eventlet/eventlet/issues/619

Read More

(译)Beyond Relational Databases:A Focus on Redis, MongoDB, and ClickHouse


percona的什么白皮书,我看就是一堆吐槽

关系型数据库不是万金油,用错反而是瓶颈

数据模型万金油,反而限制了扩展能力

sql好用但是不够灵活,json人人爱

ACID很重要,但是代价也很大,这也是关系型数据库的采用点

由于保证事务可靠,也就限制了扩展能力,数据模型简单不要求事务,上内存kv数据库

mysql也能存大数据,但是能存不能用,没啥意义,所以去掉索引,去掉关系限制,列式数据库诞生了


KV数据库

最开始的目的是做个数据库前端无状态缓存

要快,一般都是hashtable。不是为了替代关系型数据库,只是一项中间件技术

后来KV数据库发展成有简单协议的有命令有状态的服务了。不得不提redis

个人认为 redis能打败memcache,社区好,功能性强,memcache就是单纯的做无状态中间件。这种东西mysql内部也做小型cache,比如2Q啥的。不如一个组件强。但是组件本身也要考虑淘汰以及热点击穿的问题。这就是另一个话题了

典型代表redis提供了一些很好的特性,能让业务更简化,比如TTL, list,比如lua支持,LRU淘汰策略,多key事务,阻塞命令等等

做mysql前端最好不过,做个纯粹的数据库也可以(不落盘不停机)


文档数据库

最早来自邮件存储需求

json对象,sparse index 不存在的key就不要,非常灵活

灵活模式不等于没有模式

典型代表 mongodb 不止提供文档接口,还提供了分片管理,方便水平扩展,加钱上机器就行了

还有一些牛逼特性,比如聚合动作方便map-reduce ,geo相关功能,以及可插拔引擎(wiretiger/rocksdb)

劣势

嵌套表效率低

多表join查询

多表事务


列式存储

只要几列数据为什么不用覆盖索引?

  • 查询灵活难道要每一列都建索引吗?不现实
  • 插入要改动索引,复杂度爆炸延迟爆炸

作者没用Cassandra和hbase举例,而是用了clickhouse

  • 兼容大多数SQL
  • 也有索引,针对MergeTree引擎,支持主键,支持range查
  • Dictionaries 查表不用join
  • 低延迟,查询不用担心阻塞
  • 近似估计等等特殊特性

对于读敏感的大数据集,十分好用

OLTP


ref
  1. https://learn.percona.com/hubfs/ExpertOpinion/Beyond%20Relational%20Databases.pdf

Read More

改造glog 提供日志轮转


这个是同事的修改经验,写的挺有意思的,我直接抄过来了

glog是写日志的。选型pika使用,没改。存在的弊端

  • 没有日志删除。最新glog是按照日志轮转的,而不是按照个数/大小轮转
    • glog支持按照大小来切割轮转,但是不支持清理,清理,一个unlink的事儿。
  • 各种日志级别的信息是区分保存的,对于查看来说很不方便

我的同事在这两点上根据pika已有的结构来进行优化,优化的很巧妙:

  • 加上日志个数的glog接口,暴露出来,也就是实现一个接口来unlink
    • 对应的,所有的日志名字要保留到一个数组里。那这个数组的更新也要加锁
      • LogFileObject::CreateLogfile 更新数组,新增接口里改数组
    • 不能频繁检查。放到epoll时间时间里,定时触发
  • 对于第二点,在LogFileObject::Write里有具体的格式,level,去掉就行。LogDestination::FlushLogFilesUnsafe里去掉不同level的flush,改成一个

LogFileObject是具体的操作 LogDestination是底层具体的写入,会持有LogFileObject对象,所有接口从LogMessage暴露

总之挺巧妙的。学习一波经验

ref

  1. https://github.com/google/glog

Read More

^