原文連接:blog.thinkeridea.com/201907/go/c…html
咱們業務天天須要記錄大量的日誌數據,且這些數據十分重要,它們是公司收入結算的主要依據,也是數據分析部門主要得數據源,針對這麼重要的日誌,且高頻率的日誌,咱們須要一個高性能且安全的日誌組件,能保證每行日誌格式完整性,咱們設計了一個類 csv 的日誌拼接組件,它的代碼在這裏 datalog。git
它是一個能夠保證日誌各列完整性且高效拼接字段的組件,支持任意列和行分隔符,並且還支持數組字段,但是實現一對多的日誌需求,不用記錄多個日誌,也不用記錄多行。它響應一個 []byte
數據,方便結合其它主鍵寫入數據到日誌文件或者網絡中。github
NewRecord(len int) Record
建立長度固定的日誌記錄shell
NewRecordPool(len int) *sync.Pool
建立長度固定的日誌記錄緩存池編程
ToBytes(sep, newline string) []byte
使用 sep 鏈接 Record,並在末尾添加 newline 換行符數組
ArrayJoin(sep string) string
使用 sep 鏈接 Record,其結果做爲數組字段的值緩存
ArrayFieldJoin(fieldSep, arraySep string) string
使用 fieldSep 鏈接 Record,其結果做爲一個數組的單元安全
Clean()
清空 Record 中的全部元素,若是使用 sync.Pool 在放回 Pool 以前應該清空 Record,避免內存泄漏網絡
UnsafeToBytes(sep, newline string) []byte
使用 sep 鏈接 Record,並在末尾添加 newline 換行符, 使用原地替換會破壞日誌字段引用的字符串ide
UnsafeArrayFieldJoin(fieldSep, arraySep string) string
使用 fieldSep 鏈接 Record,其結果做爲一個數組的單元, 使用原地替換會破壞日誌字段引用的字符串
底層使用 type Record []string
字符串切片做爲一行或者一個數組字段,在使用時它應該是定長的,由於數據日誌每每是格式化的,每列都有本身含義,使用 NewRecord(len int) Record
或者 NewRecordPool(len int) *sync.Pool
建立組件,我建議每一個日誌使用 NewRecordPool
在程序初始化時建立一個緩存池,程序運行時從緩存次獲取 Record
將會更加高效,可是每次放回 Pool
時須要調用 Clean
清空 Record
避免引用字符串沒法被回收,而致使內存泄漏。
咱們須要保證日誌每列數據的含義一至,咱們建立了定長的 Record
,可是如何保證每列數據一致性,利用go 的常量枚舉能夠很好的保證,例如咱們定義日誌列常量:
const (
LogVersion = "v1.0.0"
)
const (
LogVer = iota
LogTime
LogUid
LogUserName
LogFriends
LogFieldNumber
)
複製代碼
LogFieldNumber
就是日誌的列數量,也就是 Record
的長度,以後使用 NewRecordPool
建立緩存池,而後使用常量名稱做爲下標記錄日誌,這樣就不用擔憂由於檢查或者疏乎致使日誌列錯亂的問題了。
var w bytes.Buffer // 一個日誌寫組件
var pool = datalog.NewRecordPool(LogFieldNumber) // 建立一個緩存池
func main() {
r := pool.Get().(datalog.Record)
r[LogVer] = LogVersion
r[LogTime] = time.Now().Format("2006-01-02 15:04:05")
// 檢查用戶數據是否存在
//if user !=nil{
r[LogUid] = "Uid"
r[LogUserName] = "UserNmae"
//}
// 拼接一行日誌數據
data := r.Join(datalog.FieldSep, datalog.NewLine)
r.Clean() // 清空 Record
pool.Put(r) // 放回到緩存池
// 寫入到日誌中
if _, err := w.Write(data); err != nil {
panic(err)
}
// 打印出日誌數據
fmt.Println("'" + w.String() + "'")
}
複製代碼
以上程序運行會輸出:
由於分隔符是不可見字符,下面使用,代替字段分隔符,使用;\n代替換行符, 使用/代替數組字段分隔符,是-代替數組分隔符。
'v1.0.0,2019-07-18,11:39:09,Uid,UserNmae,;\n'
複製代碼
即便咱們沒有記錄 LogFriends
列的數據,可是在日誌中它仍然有一個佔位符,若是 user
是 nil
,LogUid
和 LogUserName
不須要特殊處理,也不須要寫入數據,它依然佔據本身的位置,不用擔憂日誌所以而錯亂。
使用 pool 能夠很好的利用內存,不會帶來過多的內存分配,並且 Record 的每一個字段值都是字符串,簡單的賦值並不會帶來太大的開銷,它會指向字符串自己的數據,不會有額外的內存分配,詳細參見string 優化誤區及建議。 使用 Record.Join
能夠高效的鏈接一行日誌記錄,便於咱們快速的寫入的日誌文件中,後面設計講解部分會詳細介紹 Join
的設計。
有時候也並不是都是記錄一些單一的值,好比上面 LogFriends 會記錄當前記錄相關的朋友信息,這多是一組數據,datalog 也提供了一些簡單的輔助函數,能夠結合下面的實例實現:
// 定義 LogFriends 數組各列的數據
const (
LogFriendUid = iota
LogFriendUserName
LogFriendFieldNumber
)
var w bytes.Buffer // 一個日誌寫組件
var pool = datalog.NewRecordPool(LogFieldNumber) // 每行日誌的 pool
var frPool = datalog.NewRecordPool(LogFriendFieldNumber) // LogFriends 數組字段的 pool
func main(){
// 程序運行時
r := pool.Get().(datalog.Record)
r[LogVer] = LogVersion
r[LogTime] = time.Now().Format("2006-01-02 15:04:05")
// 檢查用戶數據是否存在
//if user !=nil{
r[LogUid] = "Uid"
r[LogUserName] = "UserNmae"
//}
// 拼接一個數組字段,其長度是不固定的
r[LogFriends] = GetLogFriends(rand.Intn(3))
// 拼接一行日誌數據
data := r.Join(datalog.FieldSep, datalog.NewLine)
r.Clean() // 清空 Record
pool.Put(r) // 放回到緩存池
// 寫入到日誌中
if _, err := w.Write(data); err != nil {
panic(err)
}
// 打印出日誌數據
fmt.Println("'" + w.String() + "'")
}
// 定義一個函數來拼接 LogFriends
func GetLogFriends(friendNum int) string {
// 根據數組長度建立一個 Record,數組的個數每每是不固定的,它總體做爲一行日誌的一個字段,因此並不會破壞數據
fs := datalog.NewRecord(friendNum)
// 這裏只須要中 pool 中獲取一個實例,它能夠反覆複用
fr := frPool.Get().(datalog.Record)
for i := 0; i < friendNum; i++ {
// fr.Clean() 若是不是每一個字段都賦值,應該在使用前或者使用後清空它們便於後面複用
fr[LogFriendUid] = "FUid"
fr[LogFriendUserName] = "FUserName"
// 鏈接一個數組中各個字段,做爲一個數組單元
fs[i] = fr.ArrayFieldJoin(datalog.ArrayFieldSep, datalog.ArraySep)
}
fr.Clean() // 清空 Record
frPool.Put(fr) // 放回到緩存池
// 鏈接數組的各個單元,返回一個字符串做爲一行日誌的一列
return fs.ArrayJoin(datalog.ArraySep)
}
複製代碼
以上程序運行會輸出:
由於分隔符是不可見字符,下面使用,代替字段分隔符,使用;\n代替換行符, 使用/代替數組字段分隔符,是-代替數組分隔符。
'v1.0.0,2019-07-18,11:39:09,Uid,UserNmae,FUid/FUserName-FUid/FUserName;\n'
複製代碼
這樣在解析時能夠把某一字段當作數組解析,這極大的極大的提升了數據日誌的靈活性, 可是並不建議使用過多的層級,數據日誌應當清晰簡潔,可是有些特殊場景可使用一層嵌套。
使用 ToBytes
和 ArrayFieldJoin
時會把數據字段中的鏈接字符串替換一個空字符串,因此在 datalog 裏面定義了4個分隔符,它們都是不可見字符,極少會出如今數據中,可是咱們還須要替換數據中的這些鏈接字符,避免破壞日誌結構。
雖然組件支持各類鏈接符,可是爲了不數據被破壞,咱們應該選擇一些不可見且少見的單字節字符做爲分隔符。換行符比較特殊,由於大多很多天志讀取組件都是用 \n
做爲行分隔符,若是數據中極少出現 \n
那就可使用 \n
, datalog 中定義 \x03\n
做爲換行符,它兼容通常的日誌讀取組件,只須要咱們作少許的工做就能夠正確的解析日誌了。
UnsafeToBytes
和 UnsafeArrayFieldJoin
性能會更好,和它們的名字同樣,他們並不安全,由於它們使用 exbytes.Replace 作原地替換分隔符,這會破壞數據所指向的原始字符串。除非咱們日誌數據中會出現極多的分隔符須要替換,否者並不建議使用它們,由於它們只在替換時提高性能。
我在服務中大量使用 UnsafeToBytes
和 UnsafeArrayFieldJoin
,我老是在一個請求結束時記錄日誌,我確保全部相關的數據不會再使用,因此不用擔憂原地替換致使其它數據被無感知改變的問題,這也許是一個很好的實踐,可是我仍然不推薦使用它們。
datalog 並無提供太多的約束很功能,它僅僅包含一種實踐和一組輔助工具,在使用它以前,咱們須要瞭解這些實踐。
它幫咱們建立一個定長的日誌行或者一個sync.Pool
,咱們須要結合常量枚舉記錄數據,它幫咱們把各列數據鏈接成記錄日誌須要的數據格式。
它所提供的輔助方法都通過實際項目的考驗,考量諸多細節,以高性能爲核心目標所設計,使用它能夠極大的下降相關組件的開發成本,接下來這節將分析它的各個部分。
我認爲值得說道的是它提供的一個 Join
方法,相對於 strings.Join
能夠節省兩次的內存分配,現從它開始分析。
// Join 使用 sep 鏈接 Record, 並在末尾追加 suffix
// 這個相似 strings.Join 方法,可是避免了鏈接後追加後綴(每每是換行符)致使的內存分配
// 這個方法直接返回須要的 []byte 類型, 能夠減小類型轉換,下降內存分配致使的性能問題
func (l Record) Join(sep, suffix string) []byte {
if len(l) == 0 {
return []byte(suffix)
}
n := len(sep) * (len(l) - 1)
for i := 0; i < len(l); i++ {
n += len(l[i])
}
n += len(suffix)
b := make([]byte, n)
bp := copy(b, l[0])
for i := 1; i < len(l); i++ {
bp += copy(b[bp:], sep)
bp += copy(b[bp:], l[i])
}
copy(b[bp:], suffix)
return b
}
複製代碼
日誌組件每每輸入的參數是 []byte
類型,因此它直接返回一個 []byte
,而不像 strings.Join
響應一個字符串,在末尾是須要對內部的 buf
進行類型轉換,致使額外的內存開銷。咱們每行日誌不只須要使用分隔符鏈接各列,還須要一個行分隔符做爲結尾,它提供一個後綴 suffix
,不用咱們以後在 Join
結果後再次拼接行分隔符,這樣也能減小一個額外的內存分配。
這偏偏是 datalog 設計的精髓,它並無大量使用標準庫的方法,而是設計更符合該場景的方法,以此來得到更高的性能和更好的使用體驗。
// ToBytes 使用 sep 鏈接 Record,並在末尾添加 newline 換行符
// 注意:這個方法會替換 sep 與 newline 爲空字符串
func (l Record) ToBytes(sep, newline string) []byte {
for i := len(l) - 1; i >= 0; i-- {
// 提早檢查是否包含特殊字符,以便跳過字符串替換
if strings.Index(l[i], sep) < 0 && strings.Index(l[i], newline) < 0 {
continue
}
b := []byte(l[i]) // 這會從新分配內存,避免原地替換致使引用字符串被修改
b = exbytes.Replace(b, exstrings.UnsafeToBytes(sep), []byte{' '}, -1)
b = exbytes.Replace(b, exstrings.UnsafeToBytes(newline), []byte{' '}, -1)
l[i] = exbytes.ToString(b)
}
return l.Join(sep, newline)
}
複製代碼
ToBytes
做爲很重要的交互函數,也是該組件使用頻率最高的函數,它在鏈接各個字段以前替換每一個字段中的字段和行分隔符,這裏提早作了一個檢查字段中是否包含分隔符,若是包含使用 []byte(l[i])
拷貝該列的數據,而後使用 exbytes.Replace 提供高性能的原地替換,由於輸入數據是拷貝從新分配的,因此不用擔憂原地替換會影響其它數據。
以後使用以前介紹的 Join
方法鏈接各列數據,若是使用 strings.Join
將會是 []byte(strings.Join([]string(l), sep) + newline)
這其中會增長不少次內存分配,該組件經過巧妙的設計規避這些額外的開銷,以提高性能。
// UnsafeToBytes 使用 sep 鏈接 Record,並在末尾添加 newline 換行符
// 注意:這個方法會替換 sep 與 newline 爲空字符串,替換採用原地替換,這會致使全部引用字符串被修改
// 必須明白其做用,否者將會致使意想不到的結果。可是這會大幅度減小內存分配,提高程序性能
// 我在項目中大量使用,我老是在請求最後記錄日誌,這樣我不會再訪問引用的字符串
func (l Record) UnsafeToBytes(sep, newline string) []byte {
for i := len(l) - 1; i >= 0; i-- {
b := exstrings.UnsafeToBytes(l[i])
b = exbytes.Replace(b, exstrings.UnsafeToBytes(sep), []byte{' '}, -1)
b = exbytes.Replace(b, exstrings.UnsafeToBytes(newline), []byte{' '}, -1)
l[i] = exbytes.ToString(b)
}
return l.Join(sep, newline)
}
複製代碼
UnsafeToBytes
和 ToBytes
類似只是沒有分割符檢查,由於exbytes.Replace 中已經包含了檢查,並且直接使用 exstrings.UnsafeToBytes 把字符串轉成 []byte
這不會發生數據拷貝,很是的高效,可是它不支持字面量字符串,不過我相信日誌中的數據均來自運行時分配,若是不幸包含字面量字符串,也不用太過擔憂,只要使用一個特殊的字符做爲分隔符,每每咱們編程字面量字符串並不會包含這些字符,執行 exbytes.Replace 沒有發生替換也是安全的。
// Clean 清空 Record 中的全部元素,若是使用 sync.Pool 在放回 Pool 以前應該清空 Record,避免內存泄漏
// 該方法沒有太多的開銷,能夠放心的使用,只是爲 Record 中的字段賦值爲空字符串,空字符串會在編譯時處理,沒有額外的內存分配
func (l Record) Clean() {
for i := len(l) - 1; i >= 0; i-- {
l[i] = ""
}
}
複製代碼
Clean
方法更簡單,它只是把各個列的數據替換爲空字符串,空字符串作爲一個特殊的字符,會在編譯時處理,並不會有額外的開銷,它們都指向同一塊內存。
// ArrayJoin 使用 sep 鏈接 Record,其結果做爲數組字段的值
func (l Record) ArrayJoin(sep string) string {
return exstrings.Join(l, sep)
}
// ArrayFieldJoin 使用 fieldSep 鏈接 Record,其結果做爲一個數組的單元
// 注意:這個方法會替換 fieldSep 與 arraySep 爲空字符串,替換採用原地替換
func (l Record) ArrayFieldJoin(fieldSep, arraySep string) string {
for i := len(l) - 1; i >= 0; i-- {
// 提早檢查是否包含特殊字符,以便跳過字符串替換
if strings.Index(l[i], fieldSep) < 0 && strings.Index(l[i], arraySep) < 0 {
continue
}
b := []byte(l[i]) // 這會從新分配內存,避免原地替換致使引用字符串被修改
b = exbytes.Replace(b, exstrings.UnsafeToBytes(fieldSep), []byte{' '}, -1)
b = exbytes.Replace(b, exstrings.UnsafeToBytes(arraySep), []byte{' '}, -1)
l[i] = exbytes.ToString(b)
}
return exstrings.Join(l, fieldSep)
}
複製代碼
ArrayFieldJoin
在鏈接各個字符串時會直接替換數組單元分隔符,以後直接使用 exstrings.Join 進行鏈接字符串,exstrings.Join 相對 strings.Join
的一個改進函數,由於它只有一次內存分配,較 strings.Join
節省一次,有興趣能夠去看它的源碼實現。
datalog 提供了一種實踐以及一些輔助工具,能夠幫助咱們快速的記錄數據日誌,更關心數據自己。具體程序性能能夠交給 datalog 來實現,它保證程序的性能。
後期我會計劃提供一個高效的日誌讀取組件,以便於讀取解析數據日誌,它較與通常文件讀取會更加高效且便捷,有針對性的優化日誌解析效率,敬請關注吧。
轉載:
本文做者: 戚銀(thinkeridea)
本文連接: blog.thinkeridea.com/201907/go/c…
版權聲明: 本博客全部文章除特別聲明外,均採用 CC BY 4.0 CN協議 許可協議。轉載請註明出處!