2021年3月30日 星期二

load average on linux

既然已提早完成部份 Q2 KR 項目,反正還有時間乾脆來跑一下測試。

一般我們都會使用 top 指令來監控系統整體狀況,在結果視窗右上角有一個 load average 資訊,從 man 可以得知這是指系統負載,但這個數字要如何解讀完全沒有頭緒,於是又拜託了 Google 大神。


上面文章是第一筆搜尋結果,講得蠻淺顯易懂的,一下子就把它看完了,當我正準備離開網站時,眼睛餘光瞄到下面有網友留言這篇文章內容有誤,於是又順著留言看了第二篇文章。


簡單來說,這個數字不只跟 CPU 有關,其負載還牽涉到 I/O 等,故不能單純拿這個數字除以 CPU 核心數來判斷系統是否過載。

該文作者建議先觀察這個數字在什麼數值時是表示工作良好,而當這個數字超過原本的數值時,我們就必須藉由其他的指標來確認發生什麼事。

我之所以如此關心這個議題,是因為我在測試時發現當查詢 mongoDB 的 collection 擁有大量筆數資料時,不但記憶體使用量會突然爆增,且 CPU 使用率也會超過 100% 好幾秒。

由於我要查詢的欄位並沒有建立索引,故直覺一定是針對常用查詢條件建立索引以減輕 CPU 負擔,但很明顯的我的系統負載也會從 0.2X 增加到 0.6X。

我的資料量目前是 3 千多萬筆,一個月後就會變成兩億一千多萬筆,而這還只是一個 collection 而已!

這也是為什麼我現在如此在意這個數字的原因,我必須實測才能得知最終結果。

2021/04/08 更新

連假前重新跑了一次模擬,昨天進終端機看,三個負載數字都來到 1.2X 左右,但是在 ESXi 過去一小時監控視窗數據顯示,CPU 使用率都還沒超過 30%,目前筆數已經來到四千多萬筆。

2021/04/12 更新

目前筆數來到八千多萬筆,查詢還是可以在秒級的單位內完成,15 分鐘負載來到 1.74 左右,唯一不一樣的是 ESXi 的最小 CPU 使用率已經超過 1 了。


2021/04/19 更新

目前筆數來到一億三千多萬筆,查詢還是可以在秒級的單位內完成,15 分鐘負載來到 1.5X 左右,唯一不一樣的是真正的 Server 已掛,但我的模擬程式還是很認真的工作。由於肩牓受傷,不方便低頭確認 Server 機器狀況,只好等到好一點再來看怎麼回事?

2021年3月27日 星期六

《蜀山劍俠傳》開書實測

有時候為了驗證電子書閱讀器的效能或功能,常常會買些原本不會看的書,上一次為了這個目的買的是《鋼鐵德魯伊》一書。

最近看到網路上有新舊讀墨小六的開書速度影片,原本以為因為這本書的字數關係,其他家的開書速度應該也會受影響,但據我實測的結果兩家 K 家閱讀器都是秒開,而我最愛的 Kindle Paperwhite 3 還是沒懸念的拿下冠軍寶座,我的最愛果然還是打遍天下無敵手。

https://youtu.be/I3H_eW5c9as

2021年3月26日 星期五

程式設計師最強裝備 — 心流頭盔

以 RPG 冒險遊戲的概念來看,程式設計師的最強頭部裝備一定是心流頭盔無誤!

就以我來說,上星期已經完成這一季的所有 KR 項目,故這星期已經開始提前開發下一季的 KR 項目。截至昨天為止也才不過四天,但我已經 KO 了我預計要完成的 3 項 KR 項目,而其中的 80 % 都是集中在最後兩天。

我之所以能夠有如此神速的表現,除了我這一季的項目就已經做好基本的模組化以利後續重覆使用外,最主要的原因應該是我這兩天除了開會以外都維持在心流的狀態。

一開始先聽著蘇慧倫好聽的歌聲,不到 10 分鐘後,眼中、腦中、心中都只剩下一行又一行的程式碼,就在這個時刻,蘇慧倫已經遠離我好幾個光年,耳朵中完全感受不到任何聲音,時間彷彿靜止一樣。

我一定是個很幸運的人,恰巧我的工作就是我最喜歡的事。


心流頭盔入手

防禦 + 80
智力 + 50
敏捷 + 60
精神 + 100 

2021年3月25日 星期四

購買繁體中文書這兩年半以來的回顧

書籍方面

01. Google Play Book
02. Kobo
03. Readmoo
04. Amazon.cn

總共在四家書商買書,主力是讀墨,目前共有 627 本書,1 本有聲書以及 3 本雜誌。


硬體方面

01. 2018/12 Kindle Oasis 2 ( 7 )
02. 2019/02 Onyx Boox Note Lite ( 10 )
03. 2019/04 Onyx Boox Nova Pro ( 7.8 )
04. 2019/09 Readmoo mooInk Pro ( 10 )
05. 2020/04 Kobo Clara HD ( 6 )
06. 2020/06 Hisense A5 ( 5.84 )

從 Kindle Paperwhite 3 以後,陸續買了 6 台,最常用也最喜歡的是 Kindle Paperwhite 3。

預計下一台購買產品,台灣廠商的電子紙手機或是彩色大螢幕電子書閱讀器(至少 7 吋以上),或是 Sony 的彩色電子紙產品(才有機會被國外大神破解)。

2021年3月23日 星期二

目前手上的電子書五兄弟

原本的電子書四兄弟,在經歷了 Note Lite 掛點,Nova Pro 出售後,陸續又買了 3 台電子書閱讀器,故目前總共擁有電子書五兄弟。


坦白說我的最愛還是 Kindle Paperwhite 3!還好 Amazon 不想出大尺吋或是彩色電子紙閱讀器?不然其他廠商我看只能趴在地上吃土。

目前使用時間排名如下:

Kindle Paperwhite 3
Hisense A5
Readmoo mooInk Pro 10
Kobo Clara HD
Kindoe Oasis 2

最常用的還是前面 2 個,一個拿來看繁體中文書,一個拿來上網查資料用。

在等待彩色大尺吋或是台灣廠商的電子紙手機過渡時期中,我只希望能夠早日破解 mooInk Pro,才不會浪費這一台的優點。

如果這台也像我的暗黑心法一樣那麼容易破解就好!在沒成功以前,我決定不在讀墨買書了,以表達我對不能花錢買客製化影像檔的強烈抗議XD

2021年3月22日 星期一

Google Nest Mini 入手

嚴格來說我很像個古代人,等到很多東西流行了一兩年後,我才會開始有興趣。

語音助理喇叭就是其中的一項,由於我是 Google 派的,當然是選擇 Nest Mini 來入手。

大概入手了三個星期,但我只有在洗澡時拿來聽音樂,看來我需要的應該是喇叭才對XD

雖然價錢只要一千出頭,但我發覺音質還不錯,我最常聽的是蘇慧倫的歌,聽著聽著感覺都快戀愛了。

好吧,我承認我很膚淺!如果蘇慧倫不是那麼漂亮的話,我就不會覺得歌好聽了XD

init function in Go package

撇開小程式不說,最近算是第一次把 Go 用在正式專案中,過程中不斷對架構修修改改,雖然我覺得這不啻是一件好事?表示有去思考怎樣寫對程式會更好,如果能在下筆前就先想清楚,寫起程式來應該會更行雲流水吧。

我算是以 C 語言出道的程式設計師,故很多時候我都只考慮到函數面向,坦白說光能夠適當的把函數拆分到不同的程式檔案中,這個程式已經有六成的可調整性,再來如果能夠把一些 Header 檔案的 include 相依性考慮清楚,我想九成的需求調整都不是什麼問題。

平常我在寫 Node.js 或是 Python 也是如此,Python 我不清楚,但 Node.js 是允許可以循環參照的,故只要不濫用全域變數(最多只用全域常數),大多時候應該是沒有什麼問題。

Go 本身不允許循環參照,我覺得這是一件好事,就像 Go 強制你要檢查 err != nil,藉由一些限制及規範,強迫使用者在寫程式前就先考慮清楚,其實也是方便你未來擴充程式。

以我的專案來說,我至少需要開發兩支獨立的執行檔,且這兩支用到的第三方模組都是一樣的,故當我在開發第一支執行檔時,我就打定主意使用多 package 的方式開發,這樣某些自己的 package 也可以給第二支執行檔使用。

雖然看了四、五本以上的 Go 書籍,但都已經是三年前的事,故我的程式架構在開發時覺得是順理成章的事,後來卻變成一個又一個的疑問,尤其現在開始開發第二支執行檔時特別明顯!

比如說,一個程式中有好多個 package,每個 package 都用了 package A,如果 package A 又有使用 init function,到底 init 會被執行幾次?雖然從結果來看只會執行一次,但我還是想要有個標準答案。

幸好 Go 本身就有提供 Spec 文件,不用像 C 一樣還要花錢買。於是很快的就在 Spec 中找到解答。

能夠每天在程式語言的道路上學習到新東西,那真的是一件快樂的事。

2021年3月20日 星期六

電子紙手機之未來

自從海信陸續推出了 3 支彩色電子紙手機後,我深深覺得這樣的產品應該是更有市場才對。

就以我來說,雖然拿的是智慧型手機也有申辦吃到飽的費率,但我幾乎未曾在外面看過影片,而影片運用是目前電子紙手機剩下的唯一弱點!如果像我這樣的人夠多的話,我是很看好這項產品的。

像我現在就是用 A5 手機來寫文章,除了剛才登入 Google 需要原本的手機外,我已經快兩個小時都是使用 A5 手機上網查資料,絲毫不覺得有什麼不方便。

真希望有任一個台灣廠商能出彩色電子紙的產品,即使一支手機要價 3 萬我也願意購買。

mooInk Pro 10 Tricks

昨天突發奇想是否能用一些方式把 apk 檔案放進機器並觸發安裝動作?底下是我嘗試過的方式。

01. 藉由官方傳檔軟體上傳,可惜的是不在允許的檔名範圍內。

02. 藉由 HTTP download apk 方式來觸發機器安裝軟體,實測結果:瀏覽器有發送 GET request,但機器並沒有觸發安裝動作。

03. 使用 dpt-rp1 工具把檔案上傳,但一樣不在允許範圍內。

04. 改名後使用 dpt-rpt1 工具上傳,再藉由 copy-document 指令複製,一樣無法成功。

雖然沒有收穫,還是記錄一下瀏覽器的相關資訊。

2021年3月8日 星期一

相容 mongoDB 的 memory DB - lungoDB

今天看到一個很神奇的專案,簡單來說就是一個相容 mongoDB 的 memory DB。

https://github.com/256dpi/lungo

第一個想到的用途就是可以更容易的避免使用 mongoDB 遇到的 GPL 問題。

假設我的專案是可用或可不用 mongoDB 的,則我的專案即使不使用 mongoDB,也保有功能上的完整性,也就是擁有獨立性與可區分性。故就算我使用了 mongoDB,我也不需要 open source。

自己的專案 open source 當然沒有什麼問題,但公司的專案就必須考慮清楚,有了這個專案,我就不用自己把 mongoDB 的 code 改成 SQLite 的 code。

2021年3月4日 星期四

Golang data race with fmt package

Goruntine 是 Go 語言裡面一個很重要的功能,它是一個輕量化的執行緒,由 Go runtime 負責去排程執行,據官方說法,同時開啟好幾千個也沒問題。

使用 Golang 開發專案時或多或少都會使用到 Goruntine,我們也很習慣在 Goruntine 裡面直接使用 fmt package,但 fmt 並不保證 concurrency safe。

最近我在專案中使用了 GINlogrotate,發現在某些情況下會導致 data race,底下便是我精簡過的程式碼。


package main

import (
    "bufio"
    "errors"
    "path/filepath"
    "fmt"
    "log"
    "os"
    "sync"
    "time"
)

type Options struct {
    Directory string
    FileNameFunc func() string
}

type Writer struct {
    logger *log.Logger

    opts Options

    f *os.File
    bw *bufio.Writer
    bytesWritten int64

    queue chan []byte
    pending sync.WaitGroup
    closing chan struct{}
    done chan struct{}
}

func (w *Writer) Write(p []byte) (n int, err error) {
    //p := make([]byte, len(b))
    //copy(p, b)

    select {
    case <-w.closing:
        return 0, errors.New("writer is closing")
    default:
        w.pending.Add(1)
        defer w.pending.Done()
    }

    w.queue <- p

    return len(p), nil
}

func (w *Writer) Close() error {
    close(w.closing)
    w.pending.Wait()

    close(w.queue)
    <-w.done

    if w.f != nil {
        if err := w.closeCurrentFile(); err != nil {
            return err
        }
    }

    return nil
}

func (w *Writer) listen() {
    for b := range w.queue {
        if w.f == nil {
            if err := w.rotate(); err != nil {
                w.logger.Println("Failed to create log file", err)
            }
        }

        size := int64(len(b))

        if _, err := w.bw.Write(b); err != nil {
            w.logger.Println("Failed to write to file.", err)
        }
        w.bytesWritten += size
    }

    close(w.done)
}

func (w *Writer) closeCurrentFile() error {
    if err := w.bw.Flush(); err != nil {
        return errors.New("failed to flush buffered writer")
    }

    if err := w.f.Sync(); err != nil {
        return errors.New("failed to sync current log file")
    }

    if err := w.f.Close(); err != nil {
        return errors.New("failed to close current log file")
    }

    w.bytesWritten = 0
    return nil
}

func (w *Writer) rotate() error {
    if w.f != nil {
        if err := w.closeCurrentFile(); err != nil {
            return err
        }
    }

    path := filepath.Join(w.opts.Directory, w.opts.FileNameFunc())
    f, err := newFile(path)
    if err != nil {
        return errors.New("failed to create new file")
    }

    w.bw = bufio.NewWriter(f)
    w.f = f
    w.bytesWritten = 0

    return nil
}

func New(logger *log.Logger, opts Options) (*Writer, error) {
    if _, err := os.Stat(opts.Directory); os.IsNotExist(err) {
        if err := os.MkdirAll(opts.Directory, os.ModePerm); err != nil {
            return nil, errors.New("directory does not exist and could not be created")
        }
    }

    w := &Writer{
        logger:  logger,
        opts:    opts,
        queue:   make(chan []byte, 2000),
        closing: make(chan struct{}),
        done:    make(chan struct{}),
    }

    go w.listen()

    return w, nil
}

func newFile(path string) (*os.File, error) {
    return os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
}

func test(wg *sync.WaitGroup, w *Writer) {
    i := 0

    var s2 string
    for i < 1 {
        s2 = "abcdefg"
        fmt.Println(s2)
        fmt.Fprint(w, s2)
        i++
    }

    wg.Done()
}

func FileNameFunc() (string) {
    t := time.Now()

    return fmt.Sprintf("%04d%02d%02d_%02d%02d%02d.log",
        t.Year(),
        t.Month(),
        t.Day(),
        t.Hour(),
        t.Minute(),
        t.Second(),
    )
}

func main() {
    var wg sync.WaitGroup

    logger := log.New(os.Stderr, "logrotate", 0)

    dir, err := os.Getwd()
    if err != nil {
        fmt.Println(err)
        return
    }

    opts := Options{
        Directory: dir,
        FileNameFunc: FileNameFunc,
    }

    writer, err := New(logger, opts)
    if err != nil {
        logger.Println(err)
        return
    }

    max := 2000
    i := 0
    for i < max {
        wg.Add(1)
        go test(&wg, writer)
        i++
    }

    wg.Wait()
}

這是測試的結果。

==================
WARNING: DATA RACE
Read at 0x00c00000a523 by goroutine 7:
  runtime.slicecopy()
      c:/go/src/runtime/slice.go:197 +0x0
  bufio.(*Writer).Write()
      c:/go/src/bufio/bufio.go:635 +0x3e5
  main.(*Writer).listen()
      E:/test.go:77 +0x1ab

Previous write at 0x00c00000a523 by goroutine 185:
  runtime.slicestringcopy()
      c:/go/src/runtime/slice.go:232 +0x0
  fmt.(*buffer).writeString()
      c:/go/src/fmt/print.go:82 +0x107
  fmt.(*fmt).padString()
      c:/go/src/fmt/format.go:110 +0x6f
  fmt.(*fmt).fmtS()
      c:/go/src/fmt/format.go:359 +0x75
  fmt.(*pp).fmtString()
      c:/go/src/fmt/print.go:447 +0x1ba
  fmt.(*pp).printArg()
      c:/go/src/fmt/print.go:698 +0xdcf
  fmt.(*pp).doPrint()
      c:/go/src/fmt/print.go:1161 +0x12c
  fmt.Fprint()
      c:/go/src/fmt/print.go:232 +0x6c
  main.test()
      E:/test.go:154 +0x130

Goroutine 7 (running) created at:
  main.New()
      E:/test.go:138 +0x29b
  main.main()
      E:/test.go:190 +0x271

Goroutine 185 (finished) created at:
  main.main()
      E:/test.go:200 +0x2e0
==================
==================
WARNING: DATA RACE
Read at 0x00c00008b1bd by goroutine 7:
  runtime.slicecopy()
      c:/go/src/runtime/slice.go:197 +0x0
  bufio.(*Writer).Write()
      c:/go/src/bufio/bufio.go:635 +0x3e5
  main.(*Writer).listen()
      E:/test.go:77 +0x1ab

Previous write at 0x00c00008b1bd by goroutine 702:
  runtime.slicestringcopy()
      c:/go/src/runtime/slice.go:232 +0x0
  fmt.(*buffer).writeString()
      c:/go/src/fmt/print.go:82 +0x107
  fmt.(*fmt).padString()
      c:/go/src/fmt/format.go:110 +0x6f
  fmt.(*fmt).fmtS()
      c:/go/src/fmt/format.go:359 +0x75
  fmt.(*pp).fmtString()
      c:/go/src/fmt/print.go:447 +0x1ba
  fmt.(*pp).printArg()
      c:/go/src/fmt/print.go:698 +0xdcf
  fmt.(*pp).doPrintln()
      c:/go/src/fmt/print.go:1173 +0xb4
  fmt.Fprintln()
      c:/go/src/fmt/print.go:264 +0x6c
  fmt.Println()
      c:/go/src/fmt/print.go:274 +0xc0
  main.test()
      E:/test.go:153 +0x42

Goroutine 7 (running) created at:
  main.New()
      E:/test.go:138 +0x29b
  main.main()
      E:/test.go:190 +0x271

Goroutine 702 (running) created at:
  main.main()
      E:/test.go:200 +0x2e0
==================
Found 2 data race(s)
exit status 66


從上面我們可以看到,不管是 fmt.Println 或是 fmt.Fprintf,都有機會引起 data race!

目前我的解決方式是把 buffer 先複製起來,再傳進 logrorate 的 channel 裡。

p := make([]byte, len(b))
copy(p, b)

解題靈感來自