Column: Columnar in-Memory Store with Bitmap Indexing

Columnar In-Memory Store with Bitmap Indexing

This package contains a high-performance, columnar, in-memory storage engine that supports fast querying, update and iteration with zero-allocations and bitmap indexing.

Features

  • Optimized, cache-friendly columnar data layout that minimizes cache-misses.
  • Optimized for zero heap allocation during querying (see benchmarks below).
  • Optimized batch updates/deletes, an update during a transaction takes around 12ns.
  • Support for SIMD-enabled aggregate functions such as "sum", "avg", "min" and "max".
  • Support for SIMD-enabled filtering (i.e. "where" clause) by leveraging bitmap indexing.
  • Support for columnar projection (i.e. "select" clause) for fast retrieval.
  • Support for computed indexes that are dynamically calculated based on provided predicate.
  • Support for concurrent updates using sharded latches to keep things fast.
  • Support for transaction isolation, allowing you to create transactions and commit/rollback.
  • Support for expiration of rows based on time-to-live or expiration column.
  • Support for atomic increment/decrement of numerical values, transactionally.
  • Support for primary keys for use-cases where offset can't be used.
  • Support for change data stream that streams all commits consistently.
  • Support for concurrent snapshotting allowing to store the entire collection into a file.

Documentation

The general idea is to leverage cache-friendly ways of organizing data in structures of arrays (SoA) otherwise known "columnar" storage in database design. This, in turn allows us to iterate and filter over columns very efficiently. On top of that, this package also adds bitmap indexing to the columnar storage, allowing to build filter queries using binary and, and not, or and xor (see kelindar/bitmap with SIMD support).

Collection and Columns

In order to get data into the store, you'll need to first create a Collection by calling NewCollection() method. Each collection requires a schema, which can be either specified manually by calling CreateColumn() multiple times or automatically inferred from an object by calling CreateColumnsOf() function.

In the example below we're loading some JSON data by using json.Unmarshal() and auto-creating colums based on the first element on the loaded slice. After this is done, we can then load our data by inserting the objects one by one into the collection. This is accomplished by calling InsertObject() method on the collection itself repeatedly.

data := loadFromJson("players.json")

// Create a new columnar collection
players := column.NewCollection()
players.CreateColumnsOf(data[0])

// Insert every item from our loaded data
for _, v := range data {
    players.InsertObject(v)
}

Now, let's say we only want specific columns to be added. We can do this by calling CreateColumn() method on the collection manually to create the required columns.

// Create a new columnar collection with pre-defined columns
players := column.NewCollection()
players.CreateColumn("name", column.ForString())
players.CreateColumn("class", column.ForString())
players.CreateColumn("balance", column.ForFloat64())
players.CreateColumn("age", column.ForInt16())

// Insert every item from our loaded data
for _, v := range loadFromJson("players.json") {
    players.InsertObject(v)
}

While the previous example demonstrated how to insert many objects, it was doing it one by one and is rather inefficient. This is due to the fact that each InsertObject() call directly on the collection initiates a separate transacion and there's a small performance cost associated with it. If you want to do a bulk insert and insert many values, faster, that can be done by calling Insert() on a transaction, as demonstrated in the example below. Note that the only difference is instantiating a transaction by calling the Query() method and calling the txn.Insert() method on the transaction instead the one on the collection.

players.Query(func(txn *Txn) error {
    for _, v := range loadFromJson("players.json") {
        txn.InsertObject(v)
    }
    return nil // Commit
})

Querying and Indexing

The store allows you to query the data based on a presence of certain attributes or their values. In the example below we are querying our collection and applying a filtering operation bu using WithValue() method on the transaction. This method scans the values and checks whether a certain predicate evaluates to true. In this case, we're scanning through all of the players and looking up their class, if their class is equal to "rogue", we'll take it. At the end, we're calling Count() method that simply counts the result set.

// This query performs a full scan of "class" column
players.Query(func(txn *column.Txn) error {
    count := txn.WithValue("class", func(v interface{}) bool {
        return v == "rogue"
    }).Count()
    return nil
})

Now, what if we'll need to do this query very often? It is possible to simply create an index with the same predicate and have this computation being applied every time (a) an object is inserted into the collection and (b) an value of the dependent column is updated. Let's look at the example below, we're fist creating a rogue index which depends on "class" column. This index applies the same predicate which only returns true if a class is "rogue". We then can query this by simply calling With() method and providing the index name.

An index is essentially akin to a boolean column, so you could technically also select it's value when querying it. Now, in this example the query would be around 10-100x faster to execute as behind the scenes it uses bitmap indexing for the "rogue" index and performs a simple logical AND operation on two bitmaps when querying. This avoid the entire scanning and applying of a predicate during the Query.

// Create the index "rogue" in advance
out.CreateIndex("rogue", "class", func(v interface{}) bool {
    return v == "rogue"
})

// This returns the same result as the query before, but much faster
players.Query(func(txn *column.Txn) error {
    count := txn.With("rogue").Count()
    return nil
})

The query can be further expanded as it allows indexed intersection, difference and union operations. This allows you to ask more complex questions of a collection. In the examples below let's assume we have a bunch of indexes on the class column and we want to ask different questions.

First, let's try to merge two queries by applying a Union() operation with the method named the same. Here, we first select only rogues but then merge them together with mages, resulting in selection containing both rogues and mages.

// How many rogues and mages?
players.Query(func(txn *Txn) error {
    txn.With("rogue").Union("mage").Count()
    return nil
})

Next, let's count everyone who isn't a rogue, for that we can use a Without() method which performs a difference (i.e. binary AND NOT operation) on the collection. This will result in a count of all players in the collection except the rogues.

// How many rogues and mages?
players.Query(func(txn *Txn) error {
    txn.Without("rogue").Count()
    return nil
})

Now, you can combine all of the methods and keep building more complex queries. When querying indexed and non-indexed fields together it is important to know that as every scan will apply to only the selection, speeding up the query. So if you have a filter on a specific index that selects 50% of players and then you perform a scan on that (e.g. WithValue()), it will only scan 50% of users and hence will be 2x faster.

// How many rogues that are over 30 years old?
players.Query(func(txn *Txn) error {
    txn.With("rogue").WithFloat("age", func(v float64) bool {
        return v >= 30
    }).Count()
    return nil
})

Iterating over Results

In all of the previous examples, we've only been doing Count() operation which counts the number of elements in the result set. In this section we'll look how we can iterate over the result set.

As before, a transaction needs to be started using the Query() method on the collection. After which, we can call the txn.Range() method which allows us to iterate over the result set in the transaction. Note that it can be chained right after With..() methods, as expected.

In order to access the results of the iteration, prior to calling Range() method, we need to first load column reader(s) we are going to need, using methods such as txn.String(), txn.Float64(), etc. These prepare read/write buffers necessary to perform efficient lookups while iterating.

In the example below we select all of the rogues from our collection and print out their name by using the Range() method and accessing the "name" column using a column reader which is created by calling txn.String("name") method.

players.Query(func(txn *Txn) error {
    names := txn.String("name") // Create a column reader

    return txn.With("rogue").Range(func(i uint32) {
        name, _ := names.Get()
        println("rogue name", name)
    })
})

Similarly, if you need to access more columns, you can simply create the appropriate column reader(s) and use them as shown in the example before.

players.Query(func(txn *Txn) error {
    names := txn.String("name")
    ages  := txn.Int64("age")

    return txn.With("rogue").Range(func(i uint32) {
        name, _ := names.Get()
        age,  _ := ages.Get()

        println("rogue name", name)
        println("rogue age", age)
    })
})

Taking the Sum() of a (numeric) column reader will take into account a transaction's current filtering index.

players.Query(func(txn *Txn) error {
    totalAge := txn.With("rouge").Int64("age").Sum()
    totalRouges := int64(txn.Count())

    avgAge := totalAge / totalRouges

    txn.WithInt("age", func(v float64) bool {
        return v < avgAge
    })

    // get total balance for 'all rouges younger than the average rouge'
    balance := txn.Float64("balance").Sum()
    return nil
})

Updating Values

In order to update certain items in the collection, you can simply call Range() method and use column accessor's Set() or Add() methods to update a value of a certain column atomically. The updates won't be instantly reflected given that our store supports transactions. Only when transaction is commited, then the update will be applied to the collection, allowing for isolation and rollbacks.

In the example below we're selecting all of the rogues and updating both their balance and age to certain values. The transaction returns nil, hence it will be automatically committed when Query() method returns.

players.Query(func(txn *Txn) error {
    balance := txn.Float64("balance")
    age     := txn.Int64("age")

    return txn.With("rogue").Range(func(i uint32) {
        balance.Set(10.0) // Update the "balance" to 10.0
        age.Set(50)       // Update the "age" to 50
    })
})

In certain cases, you might want to atomically increment or decrement numerical values. In order to accomplish this you can use the provided Add() operation. Note that the indexes will also be updated accordingly and the predicates re-evaluated with the most up-to-date values. In the below example we're incrementing the balance of all our rogues by 500 atomically.

players.Query(func(txn *Txn) error {
    balance := txn.Float64("balance")

    return txn.With("rogue").Range(func(i uint32) {
        balance.Add(500.0) // Increment the "balance" by 500
    })
})

Expiring Values

Sometimes, it is useful to automatically delete certain rows when you do not need them anymore. In order to do this, the library automatically adds an expire column to each new collection and starts a cleanup goroutine aynchronously that runs periodically and cleans up the expired objects. In order to set this, you can simply use Insert...() method on the collection that allows to insert an object with a time-to-live duration defined.

In the example below we are inserting an object to the collection and setting the time-to-live to 5 seconds from the current time. After this time, the object will be automatically evicted from the collection and its space can be reclaimed.

players.Insert(func(r column.Row) error {
    r.SetString("name", "Merlin")
    r.SetString("class", "mage")
    r.SetTTL(5 * time.Second) // time-to-live of 5 seconds
    return nil
})

On an interesting note, since expire column which is automatically added to each collection is an actual normal column, you can query and even update it. In the example below we query and extend the time-to-live by 1 hour using the Extend() method.

players.Query(func(txn *column.Txn) error {
    ttl := txn.TTL()
    return txn.Range(func(i uint32) {
        ttl.Extend(1 * time.Hour) // Add some time
    })
})

Transaction Commit and Rollback

Transactions allow for isolation between two concurrent operations. In fact, all of the batch queries must go through a transaction in this library. The Query method requires a function which takes in a column.Txn pointer which contains various helper methods that support querying. In the example below we're trying to iterate over all of the players and update their balance by setting it to 10.0. The Query method automatically calls txn.Commit() if the function returns without any error. On the flip side, if the provided function returns an error, the query will automatically call txn.Rollback() so none of the changes will be applied.

// Range over all of the players and update (successfully their balance)
players.Query(func(txn *column.Txn) error {
    balance := txn.Float64("balance")
    txn.Range(func(i uint32) {
        v.Set(10.0) // Update the "balance" to 10.0
    })

    // No error, transaction will be committed
    return nil
})

Now, in this example, we try to update balance but a query callback returns an error, in which case none of the updates will be actually reflected in the underlying collection.

// Range over all of the players and update (successfully their balance)
players.Query(func(txn *column.Txn) error {
    balance := txn.Float64("balance")
    txn.Range(func(i uint32) {
        v.Set(10.0) // Update the "balance" to 10.0
    })

    // Returns an error, transaction will be rolled back
    return fmt.Errorf("bug")
})

Using Primary Keys

In certain cases it is useful to access a specific row by its primary key instead of an index which is generated internally by the collection. For such use-cases, the library provides Key column type that enables a seamless lookup by a user-defined primary key. In the example below we create a collection with a primary key name using CreateColumn() method with a ForKey() column type. Then, we use InsertKey() method to insert a value.

players := column.NewCollection()
players.CreateColumn("name", column.ForKey())     // Create a "name" as a primary-key
players.CreateColumn("class", column.ForString()) // .. and some other columns

// Insert a player with "merlin" as its primary key
players.InsertKey("merlin", func(r column.Row) error {
    r.SetString("class", "mage")
    return nil
})

Similarly, you can use primary key to query that data directly, without knowing the exact offset. Do note that using primary keys will have an overhead, as it requires an additional step of looking up the offset using a hash table managed internally.

// Query merlin's class
players.QueryKey("merlin", func(r column.Row) error {
    class, _ := r.String("class")
    return nil
})

Streaming Changes

This library also supports streaming out all transaction commits consistently, as they happen. This allows you to implement your own change data capture (CDC) listeners, stream data into kafka or into a remote database for durability. In order to enable it, you can simply provide an implementation of a commit.Logger interface during the creation of the collection.

In the example below we take advantage of the commit.Channel implementation of a commit.Logger which simply publishes the commits into a go channel. Here we create a buffered channel and keep consuming the commits with a separate goroutine, allowing us to view transactions as they happen in the store.

// Create a new commit writer (simple channel) and a new collection
writer  := make(commit.Channel, 1024)
players := NewCollection(column.Options{
    Writer: writer,
})

// Read the changes from the channel
go func(){
    for commit := range writer {
        fmt.Printf("commit %v\n", commit.ID)
    }
}()

// ... insert, update or delete

On a separate note, this change stream is guaranteed to be consistent and serialized. This means that you can also replicate those changes on another database and synchronize both. In fact, this library also provides Replay() method on the collection that allows to do just that. In the example below we create two collections primary and replica and asychronously replicating all of the commits from the primary to the replica using the Replay() method together with the change stream.

// Create a primary collection
writer  := make(commit.Channel, 1024)
primary := column.NewCollection(column.Options{
    Writer: &writer,
})
primary.CreateColumnsOf(object)

// Replica with the same schema
replica := column.NewCollection()
replica.CreateColumnsOf(object)

// Keep 2 collections in sync
go func() {
    for change := range writer {
        replica.Replay(change)
    }
}()

Snapshot and Restore

The collection can also be saved in a single binary format while the transactions are running. This can allow you to periodically schedule backups or make sure all of the data is persisted when your application terminates.

In order to take a snapshot, you must first create a valid io.Writer destination and then call the Snapshot() method on the collection in order to create a snapshot, as demonstrated in the example below.

dst, err := os.Create("snapshot.bin")
if err != nil {
    panic(err)
}

// Write a snapshot into the dst
err := players.Snapshot(dst)

Conversely, in order to restore an existing snapshot, you need to first open an io.Reader and then call the Restore() method on the collection. Note that the collection and its schema must be already initialized, as our snapshots do not carry this information within themselves.

src, err := os.Open("snapshot.bin")
if err != nil {
    panic(err)
}

// Restore from an existing snapshot
err := players.Restore(src)

Complete Example

func main(){

    // Create a new columnar collection
    players := column.NewCollection()
    players.CreateColumn("serial", column.ForKey())
    players.CreateColumn("name", column.ForEnum())
    players.CreateColumn("active", column.ForBool())
    players.CreateColumn("class", column.ForEnum())
    players.CreateColumn("race", column.ForEnum())
    players.CreateColumn("age", column.ForFloat64())
    players.CreateColumn("hp", column.ForFloat64())
    players.CreateColumn("mp", column.ForFloat64())
    players.CreateColumn("balance", column.ForFloat64())
    players.CreateColumn("gender", column.ForEnum())
    players.CreateColumn("guild", column.ForEnum())

    // index on humans
    players.CreateIndex("human", "race", func(r column.Reader) bool {
        return r.String() == "human"
    })

    // index for mages
    players.CreateIndex("mage", "class", func(r column.Reader) bool {
        return r.String() == "mage"
    })

    // index for old
    players.CreateIndex("old", "age", func(r column.Reader) bool {
        return r.Float() >= 30
    })

    // Load the items into the collection
    loaded := loadFixture("players.json")
    players.Query(func(txn *column.Txn) error {
        for _, v := range loaded {
            txn.InsertObject(v)
        }
        return nil
    })

    // Run an indexed query
    players.Query(func(txn *column.Txn) error {
        name := txn.Enum("name")
        return txn.With("human", "mage", "old").Range(func(idx uint32) {
            value, _ := name.Get()
            println("old mage, human:", value)
        })
    })
}

Benchmarks

The benchmarks below were ran on a collection of 100,000 items containing a dozen columns. Feel free to explore the benchmarks but I strongly recommend testing it on your actual dataset.

cpu: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz
BenchmarkCollection/insert-8            2523     469481 ns/op    24356 B/op    500 allocs/op
BenchmarkCollection/select-at-8     22194190      54.23 ns/op        0 B/op      0 allocs/op
BenchmarkCollection/scan-8              2068     568953 ns/op      122 B/op      0 allocs/op
BenchmarkCollection/count-8           571449       2057 ns/op        0 B/op      0 allocs/op
BenchmarkCollection/range-8            28660      41695 ns/op        3 B/op      0 allocs/op
BenchmarkCollection/update-at-8      5911978      202.8 ns/op        0 B/op      0 allocs/op
BenchmarkCollection/update-all-8        1280     946272 ns/op     3726 B/op      0 allocs/op
BenchmarkCollection/delete-at-8      6405852      188.9 ns/op        0 B/op      0 allocs/op
BenchmarkCollection/delete-all-8     2073188      562.6 ns/op        0 B/op      0 allocs/op

When testing for larger collections, I added a small example (see examples folder) and ran it with 20 million rows inserted, each entry has 12 columns and 4 indexes that need to be calculated, and a few queries and scans around them.

running insert of 20000000 rows...
-> insert took 20.4538183s

running snapshot of 20000000 rows...
-> snapshot took 2.57960038s

running full scan of age >= 30...
-> result = 10200000
-> full scan took 61.611822ms

running full scan of class == "rogue"...
-> result = 7160000
-> full scan took 81.389954ms

running indexed query of human mages...
-> result = 1360000
-> indexed query took 608.51µs

running indexed query of human female mages...
-> result = 640000
-> indexed query took 794.49µs

running update of balance of everyone...
-> updated 20000000 rows
-> update took 214.182216ms

running update of age of mages...
-> updated 6040000 rows
-> update took 81.292378ms

Contributing

We are open to contributions, feel free to submit a pull request and we'll review it as quickly as we can. This library is maintained by Roman Atachiants

Download Details:

Author: Kelindar
Source Code: https://github.com/kelindar/column 
License: MIT license

#go #golang #db #data 

What is GEEK

Buddha Community

Column: Columnar in-Memory Store with Bitmap Indexing
Kole  Haag

Kole Haag

1602403200

What is NoSQL and How is it Utilized?

Posted on September 25, 2020 by Dean Conally | Updated: October 8, 2020

Category: Tutorials | Tags: CassandraColumnsDatabaseDatabase ManagementDatabase StructureDB2Document StoresDynamic SchemaExtensible Record StoresGraph StoresJSONKey-ValueMSSQLMulti-RowMySQLNodeNode Relationship NodeNon-Relational DatabasesNoSQLNoSQL ModelQueryRowsScalabilitySchema FreeSQLStoresTablesWide-Column

Reading Time: 5 minutes

What is NoSQL?

A NoSQL or a NoSQL Database is a term used when referring to a “non SQL” or “not only SQL” database. NoSQL databases store data in a different format than a traditional relational database management systems. This is why NoSQL is often associated with the term “non-relational” database. Simply put, NoSQL databases are modern databases with high flexibility, blazing performance, and built for scalability. These databases are used when you require low latency and high extensibility while working with large data structures. The versatility of NoSQL is due to the nature of as being unrestricted in comparison to relational databases models such as MySQL or DB2.

SQL vs. NoSQL Comparison

There are multiple differences between SQL and NoSQL database types. In the table below, we will compare some of the most critical variations.

#tutorials #cassandra #columns #database #database management #database structure #db2 #document stores #dynamic schema #extensible record stores #graph stores #json #key-value #mssql #multi-row #mysql #node #node relationship node #non-relational databases #nosql #nosql model #query #rows #scalability #schema free #sql #stores #tables #wide-column

Trung  Nguyen

Trung Nguyen

1646796184

Vượt Qua Kỳ Thi Chuyên Gia Bảo Mật Kubernetes Được Chứng Nhận

Bài viết này dựa trên kinh nghiệm của tôi khi học và vượt qua kỳ thi Chuyên gia bảo mật Kubernetes được chứng nhận. Tôi đã vượt qua kỳ thi trong lần thử đầu tiên vào tháng 9 năm 2021.

Tôi đã vượt qua kỳ thi Nhà phát triển ứng dụng Kubernetes được chứng nhận vào tháng 2 năm 2020, tiếp theo là Quản trị viên Kubernetes được chứng nhận vào tháng 3 năm 2020.

Kỳ thi CKS hoặc Chuyên gia bảo mật Kubernetes được chứng nhận đã được phát hành vào khoảng tháng 11 năm 2020, nhưng tôi không có cơ hội tham gia kỳ thi đó trước tháng 9 năm 2021.

Như một chút thông tin cơ bản, tôi đã làm việc với Kubernetes trong 3 năm qua gần như hàng ngày và kinh nghiệm đó là một lợi thế bổ sung giúp tôi vượt qua CKS.

Trong bài viết này, tôi sẽ chia sẻ một số tài nguyên sẽ giúp bạn học tập và vượt qua kỳ thi, cùng với một bảng đánh giá hữu ích mà bạn có thể sử dụng khi chuẩn bị. Tôi cũng sẽ chia sẻ một số lời khuyên sẽ giúp ích cho bạn trong suốt quá trình.

Kubernetes là gì?

Kubernetes là hệ thống Điều phối vùng chứa phong phú và phát triển nhất hiện có và nó tiếp tục trở nên tốt hơn.

Nó có một cộng đồng khổng lồ để hỗ trợ và nó luôn xây dựng các tính năng mới và giải quyết các vấn đề. Kubernetes chắc chắn đang phát triển với tốc độ chóng mặt, và nó trở thành một thách thức để theo kịp tốc độ phát triển của nó. Điều này làm cho nó trở thành lựa chọn tốt nhất cho giải pháp điều phối vùng chứa.


Tài nguyên cho kỳ thi

Sau đây là một số tài nguyên tuyệt vời có sẵn để vượt qua kỳ thi CKS:

  1. Chuyên gia bảo mật Kubernetes được chứng nhận bởi Killer.sh
  2. Chuyên gia bảo mật Kubernetes được chứng nhận (CKS) bởi KodeKloud
  3. Walid Shaari đã tập hợp một số tài liệu không thể thiếu cho kỳ thi CKS
  4. Tài liệu tham khảo của Abdennour cho các mục tiêu kỳ thi CKS
  5. Bộ sưu tập tài nguyên của Ibrahim Jelliti để chuẩn bị cho kỳ thi Chuyên gia bảo mật Kubernetes được chứng nhận (CKSS)

Các khóa học cho KodeKloud và Killer.sh cung cấp các trình mô phỏng kỳ thi thử rất hữu ích trong việc chuẩn bị cho kỳ thi và cung cấp một ý tưởng khá tốt về kỳ thi trông như thế nào. Tôi thực sự khuyên bạn nên đăng ký vào một hoặc cả hai khóa học.

Mua bài kiểm tra từ Linux Foundation mang đến cho bạn 2 lần thử miễn phí trong trình mô phỏng kỳ thi từ killer.sh. Bằng cách đó, nếu bạn đã thành thạo với nội dung của chương trình học, bạn có thể bỏ qua các khóa học và trực tiếp đến với trình mô phỏng kỳ thi được cung cấp kèm theo kỳ thi.

Kỳ thi có giá $ 375 nhưng có các ưu đãi và giao dịch có sẵn, và nếu bạn tìm kiếm chúng, bạn có thể có được mức giá tốt hơn. Thời gian của kỳ thi là 2 giờ và có giá trị trong 2 năm, không giống như CKA và CKAD có giá trị trong 3 năm.

Bí danh

CKS là một kỳ thi dựa trên thành tích, nơi bạn được cung cấp một trình mô phỏng kỳ thi mà bạn phải tìm ra các vấn đề. Bạn chỉ được phép mở một tab ngoài tab kỳ thi.

Vì kỳ thi này yêu cầu bạn viết rất nhiều lệnh, tôi đã sớm nhận ra rằng tôi sẽ phải dựa vào bí danh để giảm số lần nhấn phím nhằm tiết kiệm thời gian.

Tôi đã sử dụng trình soạn thảo vi trong suốt kỳ thi, vì vậy ở đây tôi sẽ chia sẻ một số mẹo hữu ích cho trình soạn thảo này.

vi mặc định cho ~ / .vimrc:

vi ~/.vimrc
---
:set number
:set et
:set sw=2 ts=2 sts=2
---
^: Start of word in line
0: Start of line
$: End of line
w: End of word
GG: End of file

kubectl mặc định cho ~ / .bashrc:

vi ~/.bashrc
---
alias k='kubectl'
alias kg='k get'
alias kd='k describe'
alias kl='k logs'
alias ke='k explain'
alias kr='k replace'
alias kc='k create'
alias kgp='k get po'
alias kgn='k get no'
alias kge='k get ev'
alias kex='k exec -it'
alias kgc='k config get-contexts'
alias ksn='k config set-context --current --namespace'
alias kuc='k config use-context'
alias krun='k run'
export do='--dry-run=client -oyaml'
export force='--grace-period=0 --force'

source <(kubectl completion bash)
source <(kubectl completion bash | sed 's/kubectl/k/g' )
complete -F __start_kubectl k


alias krp='k run test --image=busybox --restart=Never'
alias kuc='k config use-context'
---

Các phím tắt

Lệnh kubectl get này cung cấp các tên ngắn gọn hấp dẫn để truy cập tài nguyên và tương tự như pvc đối với persistentstorageclaim. Những điều này có thể giúp tiết kiệm rất nhiều thao tác gõ phím và thời gian quý báu trong kỳ thi.

  • po cho pods
  • rs cho replicasets
  • triển khai cho deployments
  • svc cho services
  • ns cho namespace
  • netpol cho networkpolicy
  • pv cho persistentstorage
  • pvc cho persistentstorageclaim
  • sa cho serviceaccounts

Kubernetes Cheat Sheet

lệnh chạy kubectl

Lệnh kubectl run cung cấp một cờ --restart cho phép bạn tạo các loại đối tượng Kubernetes khác nhau từ Triển khai đến CronJob.

Đoạn mã dưới đây cho thấy các tùy chọn khác nhau có sẵn cho --restart cờ.

k run:
--restart=Always                             #Creates a deployment
--restart=Never                              #Creates a Pod
--restart=OnFailure                          #Creates a Job
--restart=OnFailure --schedule="*/1 * * * *" #Creates a CronJob

Cách tạo thông số yaml từ một nhóm hiện có

Đôi khi, việc tạo một thông số kỹ thuật từ một nhóm hiện có và thực hiện các thay đổi đối với nó dễ dàng hơn là tạo một nhóm mới từ đầu. Lệnh kubectl get pod cung cấp cho chúng ta các cờ cần thiết để xuất ra thông số nhóm ở định dạng chúng ta muốn.

kgp <pod-name> -o wide

# Generating YAML Pod spec
kgp <pod-name> -o yaml
kgp <pod-name> -o yaml > <pod-name>.yaml

# Get a pod's YAML spec without cluster specific information
kgp my-pod -o yaml --export > <pod-name>.yaml

lệnh pod kubectl

Lệnh kubectl run cung cấp rất nhiều tùy chọn, chẳng hạn như chỉ định các yêu cầu và giới hạn mà một nhóm phải sử dụng hoặc các lệnh mà một vùng chứa sẽ chạy sau khi được tạo.

# Output YAML for a nginx pod running an echo command
krun nginx --image=nginx --restart=Never --dry-run -o yaml -- /bin/sh -c 'echo Hello World!'
# Output YAML for a busybox pod running a sleep command
krun busybox --image=busybox:1.28 --restart=Never --dry-run -o yaml -- /bin/sh -c 'while true; do echo sleep; sleep 10; done'
# Run a pod with set requests and limits
krun nginx --image=nginx --restart=Never --requests='cpu=100m,memory=512Mi' --limits='cpu=300m,memory=1Gi'
# Delete pod without delay
k delete po busybox --grace-period=0 --force

Cách in nhật ký và xuất chúng

Nhật ký là nguồn thông tin cơ bản khi nói đến gỡ lỗi một ứng dụng. Lệnh kubectl logs cung cấp chức năng kiểm tra nhật ký của một nhóm nhất định. Bạn có thể sử dụng các lệnh dưới đây để kiểm tra nhật ký của một nhóm nhất định.

kubectl logs deploy/<podname>
kubectl logs deployment/<podname>
#Follow logs
kubectl logs deploy/<podname> --tail 1 --follow

Ngoài việc chỉ xem nhật ký, chúng tôi cũng có thể xuất nhật ký thành tệp để gỡ lỗi thêm khi chia sẻ cùng một tệp với bất kỳ ai.

kubectl logs <podname> --namespace <ns> > /path/to/file.format

Cách tạo bản đồ cấu hình và bí mật

Lệnh kubectl create cho phép chúng tôi tạo Bản đồ cấu hình và Bí mật từ dòng lệnh. Chúng tôi cũng có thể sử dụng tệp YAML để tạo cùng một tài nguyên và bằng cách sử dụng kubectl apply -f <filename>, chúng tôi có thể áp dụng các lệnh.

kc cm my-cm --from-literal=APP_ENV=dev
kc cm my-cm --from-file=test.txt
kc cm my-cm --from-env-file=config.env

kc secret generic my-secret --from-literal=APP_SECRET=sdcdcsdcsdcsdc
kc secret generic my-secret --from-file=secret.txt
kc secret generic my-secret --from-env-file=secret.env

Các lệnh hữu ích để gỡ lỗi

Gỡ lỗi là một kỹ năng rất quan trọng khi bạn đang đối mặt với các vấn đề và lỗi trong công việc hàng ngày của chúng tôi và khi giải quyết các vấn đề trong kỳ thi CKS.

Ngoài khả năng xuất nhật ký từ vùng chứa, các kubectl exec lệnh cho phép bạn đăng nhập vào vùng chứa đang chạy và gỡ lỗi các vấn đề. Khi ở bên trong vùng chứa, bạn cũng có thể sử dụng các tiện ích như ncnslookup để chẩn đoán các sự cố liên quan đến mạng.

# Run busybox container
k run busybox --image=busybox:1.28 --rm --restart=Never -it sh
# Connect to a specific container in a Pod
k exec -it busybox -c busybox2 -- /bin/sh
# adding limits and requests in command
kubectl run nginx --image=nginx --restart=Never --requests='cpu=100m,memory=256Mi' --limits='cpu=200m,memory=512Mi'
# Create a Pod with a service
kubectl run nginx --image=nginx --restart=Never --port=80 --expose
# Check port
nc -z -v -w 2 <service-name> <port-name>
# NSLookup
nslookup <service-name>
nslookup 10-32-0-10.default.pod

Cập nhật lần lượt và triển khai

Lệnh kubectl rollout cung cấp khả năng kiểm tra trạng thái của các bản cập nhật và nếu được yêu cầu, quay trở lại phiên bản trước đó.

k set image deploy/nginx nginx=nginx:1.17.0 --record
k rollout status deploy/nginx
k rollout history deploy/nginx
# Rollback to previous version
k rollout undo deploy/nginx
# Rollback to revision number
k rollout undo deploy/nginx --to-revision=2
k rollout pause deploy/nginx
k rollout resume deploy/nginx
k rollout restart deploy/nginx
kubectl run nginx-deploy --image=nginx:1.16 --replias=1 --record

Lệnh scale và autoscale

Lệnh kubectl scale cung cấp chức năng mở rộng hoặc thu nhỏ các nhóm trong một triển khai nhất định.

Sử dụng kubectl autoscale lệnh, chúng tôi có thể xác định số lượng nhóm tối thiểu sẽ chạy cho một triển khai nhất định và số lượng nhóm tối đa mà việc triển khai có thể mở rộng cùng với các tiêu chí mở rộng như tỷ lệ phần trăm CPU.

k scale deploy/nginx --replicas=6
k autoscale deploy/nginx --min=3 --max=9 --cpu-percent=80

Chính sách mạng

Trong một cụm Kubernetes, tất cả các nhóm có thể giao tiếp với tất cả các nhóm theo mặc định, đây có thể là một vấn đề bảo mật trong một số triển khai.

Để giải quyết vấn đề này, Kubernetes đã giới thiệu Chính sách mạng để cho phép hoặc từ chối lưu lượng truy cập đến và đi từ các nhóm dựa trên các nhãn nhóm là một phần của thông số nhóm.

Ví dụ dưới đây từ chối cả lưu lượng vào và ra cho các nhóm đang chạy trong tất cả các không gian tên.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: example
  namespace: default
spec:
  podSelector: {}
  policyTypes:
  - Egress
  - Ingress

Ví dụ dưới đây từ chối cả lưu lượng vào và ra cho các nhóm đang chạy trong tất cả các không gian tên. Nhưng nó cho phép truy cập vào các dịch vụ phân giải DNS chạy trên cổng 53.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny
  namespace: default
spec:
  podSelector: {}
  policyTypes:
  - Egress
  - Ingress
  egress:
  - to:
    ports:
      - port: 53
        protocol: TCP
      - port: 53
        protocol: UDP

Ví dụ dưới đây từ chối quyền truy cập vào Máy chủ siêu dữ liệu đang chạy trên địa chỉ IP 169.256.169.256trong Phiên bản AWS EC2.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name:cloud-metadata-deny
  namespace: default
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
      - ipBlock: 
          cidr: 0.0.0.0/0
          except:
          - 169.256.169.256/32

Ví dụ dưới đây cho phép Truy cập vào máy chủ siêu dữ liệu đang chạy trên địa chỉ IP 169.256.169.256trong Phiên bản AWS EC2.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: cloud-metadata-accessor
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: metadata-accessor
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 169.256.169.256/32

Phân tích tĩnh bằng Kubesec

Kubesec là một công cụ Phân tích tĩnh để phân tích các tệp YAML để tìm ra các vấn đề với tệp.

kubesec scan pod.yaml

# Using online kubesec API
curl -sSX POST --data-binary @pod.yaml https://v2.kubesec.io/scan

# Running the API locally
kubesec http 8080 &

kubesec scan pod.yaml -o pod_report.json -o json

Quét lỗ hổng bảo mật bằng Trivvy

Trivvy là một công cụ Quét lỗ hổng bảo mật để quét các hình ảnh vùng chứa để tìm các vấn đề bảo mật.

trivy image nginx:1.18.0
trivy image --severity CRITICAL nginx:1.18.0
trivy image --severity CRITICAL, HIGH nginx:1.18.0
trivy image --ignore-unfixed nginx:1.18.0

# Scanning image tarball
docker save nginx:1.18.0 > nginx.tar
trivy image --input archive.tar

# Scan and output results to file
trivy image --output python_alpine.txt python:3.10.0a4-alpine
trivy image --severity HIGH --output /root/python.txt python:3.10.0a4-alpine

# Scan image tarball
trivy image --input alpine.tar --format json --output /root/alpine.json

Cách xóa các dịch vụ không mong muốn

Tính năng này systemctl cho thấy các khả năng khởi động, dừng, bật, tắt và liệt kê các dịch vụ đang chạy trên Máy ảo Linux.

Liệt kê các dịch vụ:

systemctl list-units --type service

Dừng phục vụ:

systemctl stop apache2

Tắt dịch vụ:

systemctl disable apache2

Xóa dịch vụ:

apt remove apache2

Các lớp thời gian chạy

Kubernetes đã giới thiệu tính năng RuntimeClass trong phiên bản v1.12để chọn cấu hình thời gian chạy vùng chứa. Cấu hình thời gian chạy của vùng chứa được sử dụng để chạy các vùng chứa bên dưới của một nhóm.

Hầu hết các cụm Kubernetes sử dụng dockershim làm lớp Thời gian chạy cho các vùng chứa đang chạy, nhưng bạn có thể sử dụng Thời gian chạy vùng chứa khác nhau.

Phiên dockershim bản Kubernetes đã không còn được dùng nữa v1.20và sẽ bị xóa trong v1.24.

Cách tạo một Lớp thời gian chạy:

apiversion: node.k8s.io/v1beta1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc

Cách sử dụng một lớp thời gian chạy cho bất kỳ nhóm nào đã cho:

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: nginx
  name: nginx
spec:
  runtimeClassName: gvisor
  containers:
  - name: nginx
    image: nginx 

Lệnh RBAC

Trong các chính phủ,

Các lệnh kiểm soát truy cập dựa trên vai trò (RBAC) cung cấp một phương pháp điều chỉnh quyền truy cập vào tài nguyên Kubernetes dựa trên vai trò của từng người dùng hoặc tài khoản dịch vụ. ( Nguồn )

Đây là cách tạo một vai trò:

kubectl create role developer --resource=pods --verb=create,list,get,update,delete --namespace=development

Cách tạo ràng buộc vai trò:

kubectl create rolebinding developer-role-binding --role=developer --user=faizan --namespace=development

Cách xác thực:

kubectl auth can-i update pods --namespace=development --as=faizan

Cách tạo vai trò cụm:

kubectl create clusterrole pvviewer-role --resource=persistentvolumes --verb=list

Và cách tạo liên kết Clusterrole Binding với tài khoản dịch vụ:

kubectl create clusterrolebinding pvviewer-role-binding --clusterrole=pvviewer-role --serviceaccount=default:pvviewer

Bảo trì cụm

Bạn sử dụng kubectl drain lệnh để xóa tất cả khối lượng công việc đang chạy (nhóm) khỏi một Node nhất định.

Bạn sử dụng kubectl cordon lệnh để buộc một nút để đánh dấu nó là có thể lập lịch.

Bạn sử dụng kubectl uncordon lệnh để đặt nút là có thể lập lịch, nghĩa là Trình quản lý bộ điều khiển có thể lập lịch các nhóm mới cho nút đã cho.

Cách thoát một nút của tất cả các nhóm:

kubectl drain node-1

Làm thế nào để rút một nút và bỏ qua daemonsets:

kubectl drain node01 --ignore-daemonsets

Làm thế nào để buộc thoát nước:

kubectl drain node02 --ignore-daemonsets --force

Cách đánh dấu một nút là không thể lập lịch để không có nhóm mới nào có thể được lập lịch trên nút này:

kubectl cordon node-1

Đánh dấu một nút có thể lập lịch

kubectl uncordon node-1

Mẹo thi CKS

Lệnh Kubernetes kubectl get cung cấp cho người dùng cờ đầu ra -o hoặc --output giúp chúng tôi định dạng đầu ra ở dạng JSON, yaml, wide hoặc tùy chỉnh-cột.

JSON và JSONPath

Cách xuất nội dung của tất cả các nhóm ở dạng Đối tượng JSON:

kubectl get pods -o json

JSONPath xuất ra một khóa cụ thể từ Đối tượng JSON:

kubectl get pods -o=jsonpath='{@}'
kubectl get pods -o=jsonpath='{.items[0]}'

Được sử dụng khi chúng ta có nhiều đối tượng , .items[*]ví dụ như nhiều vùng chứa với cấu hình nhóm:

# For list of items use .items[*]
k get pods -o 'jsonpath={.items[*].metadata.labels.version}'
# For single item
k get po busybox -o jsonpath='{.metadata}'
k get po busybox -o jsonpath="{['.metadata.name', '.metadata.namespace']}{'\n'}"

Lệnh trả về IP nội bộ của một Node sử dụng JSONPath:

kubectl get nodes -o=jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}'

Lệnh kiểm tra sự bình đẳng trên một khóa cụ thể:

kubectl get pod api-stag-765797cf-lrd8q -o=jsonpath='{.spec.volumes[?(@.name=="api-data")].persistentVolumeClaim.claimName}'
kubectl get pod -o=jsonpath='{.items[*].spec.tolerations[?(@.effect=="NoSchedule")].key}'

Các Cột Tùy chỉnh rất hữu ích để xuất ra các trường cụ thể:

kubectl get pods -o='custom-columns=PODS:.metadata.name,Images:.spec.containers[*].image'

Chủ đề kỳ thi CKS

Kỳ thi CKS bao gồm các chủ đề liên quan đến bảo mật trong hệ sinh thái Kubernetes. Bảo mật Kubernetes là một chủ đề rộng lớn cần đề cập trong một bài báo, vì vậy bài viết này bao gồm một số chủ đề được đề cập trong kỳ thi.

Cách bảo mật và làm cứng hình ảnh vùng chứa

Trong khi thiết kế hình ảnh vùng chứa để chạy mã của bạn, hãy đặc biệt chú ý đến các biện pháp bảo mật và tăng cường để ngăn chặn các vụ hack và tấn công leo thang đặc quyền. Hãy ghi nhớ những điểm dưới đây khi xây dựng hình ảnh vùng chứa:

  1. Sử dụng các phiên bản gói cụ thể như alpine:3.13.
  2. Không chạy dưới quyền root - sử dụng USER <username>để chặn quyền truy cập root.
  3. Đặt hệ thống tệp ở chế độ chỉ đọc khi securityContext sử dụng readOnlyRootFilesystem: true
  4. Xóa quyền truy cập shell bằng cách sử dụng RUN rm -rf /bin/*

Cách giảm thiểu dấu chân của hệ điều hành

Các lớp chứa

Hướng dẫn RUNCOPY tạo ADD các lớp vùng chứa. Các hướng dẫn khác tạo hình ảnh trung gian tạm thời và không làm tăng kích thước của bản dựng. Các hướng dẫn tạo lớp sẽ bổ sung vào kích thước của hình ảnh kết quả.

Một Dockerfile điển hình trông giống như một tệp được đưa ra bên dưới. Nó thêm một lớp duy nhất bằng cách sử dụng RUN hướng dẫn.

FROM ubuntu

RUN apt-get update && apt-get install -y golang-go

CMD ["sh"]

Bản dựng nhiều giai đoạn

Multi-Stage xây dựng đòn bẩy nhiều FROM câu lệnh trong Dockerfile. Hướng FROM dẫn đánh dấu một giai đoạn mới trong quá trình xây dựng. Nó kết hợp nhiều FROM câu lệnh cho phép tận dụng từ bản dựng trước để sao chép có chọn lọc các tệp nhị phân sang giai đoạn xây dựng mới loại bỏ các mã nhị phân không cần thiết. Hình ảnh Docker kết quả có kích thước nhỏ hơn đáng kể với bề mặt tấn công giảm đáng kể.

FROM ubuntu:20.04 AS build
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go

FROM alpine:3.13
RUN chmod a-w /etc
RUN addgroup -S appgroup && adduser -S appuser -G appgroup -h /home/appuser
RUN rm -rf /bin/*
COPY --from=build /app /home/appuser/
USER appuser
CMD ["/home/appuser/app"]

Cách giới hạn quyền truy cập vào nút

Các tệp Kiểm soát Truy cập chứa thông tin nhạy cảm về người dùng / nhóm trong Hệ điều hành Linux.

#Stores information about the UID/GID, user shell, and home directory for a user
/etc/passwd
#Stores the user password in a hashed format
/etc/shadow
#Stores information about the group a user belongs
/etc/group
#Stored information about the Sudoers present in the system
/etc/sudoers

Vô hiệu hóa tài khoản người dùng giúp đảm bảo quyền truy cập vào Node bằng cách tắt đăng nhập vào một tài khoản người dùng nhất định.

usermod -s /bin/nologin <username>

Việc vô hiệu hóa root tài khoản người dùng có ý nghĩa đặc biệt, vì tài khoản gốc có tất cả các khả năng.

usermod -s /bin/nologin root

Đây là cách thêm người dùng với thư mục chính và trình bao:

adduser --home /opt/faizanbashir --shell /bin/bash --uid 2328 --ingroup admin faizanbashir
useradd -d /opt/faizanbashir -s /bin/bash -G admin -u 2328 faizanbashir

Cách xóa tài khoản người dùng:

userdel <username>

Cách xóa một nhóm:

groupdel <groupname>

Cách thêm người dùng vào nhóm:

adduser <username> <groupname>

Cách xóa người dùng khỏi nhóm:

#deluser faizanbashir admin
deluser <username> <groupname>

Cách đặt mật khẩu cho người dùng:

passwd <username>

Cách nâng cao người dùng lên thành sudoer:

vim /etc/sudoers
>>>
faizanbashir ALL=(ALL:ALL) ALL

Cách bật sudo không cần mật khẩu:

vim /etc/sudoers
>>>
faizanbashir ALL=(ALL) NOPASSWD:ALL

visudo
usermod -aG sudo faizanbashir
usermod faizanbashir -G admin

Làm cứng SSH

Cách tắt SSH

Cấu hình được đưa ra trong /etc/ssh/sshd_config có thể được tận dụng để bảo mật quyền truy cập SSH vào các nút Linux. Đặt PermitRootLogin để no tắt đăng nhập gốc trên một nút.

Để thực thi việc sử dụng khóa để đăng nhập và vô hiệu hóa đăng nhập bằng mật khẩu vào các nút, bạn có thể đặt PasswordAuthentication thành no.

vim /etc/ssh/sshd_config
>>
PermitRootLogin no
PasswordAuthentication no
<<
# Restart SSHD Service
systemctl restart sshd

Cách đặt không có đăng nhập cho người dùng root:

usermod -s /bin/nologin root

SSH Sao chép khóa người dùng / SSH không mật khẩu:

ssh-copy-id -i ~/.ssh/id_rsa.pub faizanbashir@node01
ssh faizanbashir@node01

Cách xóa các gói và dịch vụ lỗi thời

Đây là cách bạn có thể liệt kê tất cả các dịch vụ đang chạy trên máy Ubuntu:

systemctl list-units --type service
systemctl list-units --type service --state running

Cách dừng, tắt và xóa một dịch vụ:

systemctl stop apache2
systemctl disable apache2
apt remove apache2

Cách hạn chế mô-đun hạt nhân

Trong Linux, mô-đun Kernel là những đoạn mã có thể được tải và dỡ xuống kernel theo yêu cầu. Chúng mở rộng chức năng của hạt nhân mà không cần khởi động lại hệ thống. Một mô-đun có thể được cấu hình dưới dạng tích hợp sẵn hoặc có thể tải được.

Cách liệt kê tất cả các Mô-đun nhân:

lsmod

Cách tải thủ công mô-đun vào Kernel:

modprobe pcspkr

Cách đưa vào danh sách đen một mô-đun: (Tham khảo: CIS Benchmarks -> 3.4 Giao thức mạng không phổ biến)

cat /etc/modprobe.d/blacklist.conf
>>>
blacklist sctp
blacklist dccp

# Shutdown for changes to take effect
shutdown -r now

# Verify
lsmod | grep dccp

Cách xác định và tắt các cổng đang mở

Cách kiểm tra các cổng đang mở:

netstat -an | grep -w LISTEN
netstat -natp | grep 9090

nc -zv <hostname|IP> 22
nc -zv <hostname|IP> 10-22

ufw deny 8080

Cách kiểm tra việc sử dụng cổng:

/etc/services | grep -w 53

Đây là tài liệu tham khảo cho danh sách các cổng đang mở .

Cách hạn chế quyền truy cập mạng

Cách xác định một dịch vụ đang chạy trên cổng:

systemctl status ssh
cat /etc/services | grep ssh
netstat -an | grep 22 | grep -w LISTEN

Tường lửa UFW

Tường lửa không phức tạp (UFW) là một công cụ để quản lý các quy tắc tường lửa trong Arch Linux, Debian hoặc Ubuntu. UFW cho phép bạn cho phép và chặn lưu lượng truy cập trên một cổng nhất định và từ một nguồn nhất định.

Đây là cách cài đặt Tường lửa UFW:

apt-get update
apt-get install ufw
systemctl enable ufw
systemctl start ufw
ufw status
ufw status numbered

Cách cho phép tất cả các kết nối đi và đến:

ufw default allow outgoing
ufw default allow incoming

Cách cho phép các quy tắc:

ufw allow 22
ufw allow 1000:2000/tcp
ufw allow from 172.16.238.5 to any port 22 proto tcp
ufw allow from 172.16.238.5 to any port 80 proto tcp
ufw allow from 172.16.100.0/28 to any port 80 proto tcp

Cách từ chối các quy tắc:

ufw deny 8080

Cách bật và kích hoạt Tường lửa:

ufw enable

Cách xóa các quy tắc:

ufw delete deny 8080
ufw delete <rule-line>

Cách đặt lại quy tắc:

ufw reset

Linux Syscalls

Linux Syscalls được sử dụng để thực hiện các yêu cầu từ không gian người dùng vào nhân Linux. Ví dụ: trong khi tạo tệp, không gian người dùng yêu cầu Nhân Linux tạo tệp.

Kernel Space có những thứ sau:

  • Mã hạt nhân
  • Phần mở rộng Kernel
  • Trình điều khiển thiết bị

Cách theo dõi Syscalls bằng Strace

Đây là cách bạn có thể theo dõi các cuộc gọi tổng hợp bằng cách sử dụng strace:

which strace
strace touch /tmp/error.log

Cách lấy PID của một dịch vụ:

pidof sshd
strace -p <pid>

Cách liệt kê tất cả các cuộc gọi tổng hợp được thực hiện trong một hoạt động:

strace -c touch /tmp/error.log

Cách hợp nhất các cuộc gọi hệ thống danh sách: (Đếm và tóm tắt)

strace -cw ls /

Cách theo dõi PID và hợp nhất:

strace -p 3502 -f -cw

AquaSec Tracee

AquaSec Tracee được tạo ra bởi Aqua Security, sử dụng eBPF để theo dõi các sự kiện trong vùng chứa. Tracee sử dụng eBPF (Bộ lọc gói Berkeley mở rộng) trong thời gian chạy trực tiếp trong không gian hạt nhân mà không can thiệp vào nguồn hạt nhân hoặc tải bất kỳ mô-đun hạt nhân nào.

  • Nhị phân được lưu trữ tại/tmp/tracee
  • Cần quyền truy cập vào phần sau, ở chế độ chỉ đọc nếu chạy bằng vùng chứa có --privileged khả năng:
    • /tmp/tracee-> Không gian làm việc mặc định
    • /lib/modules-> Tiêu đề hạt nhân
    • /usr/src-> Tiêu đề hạt nhân

Làm thế nào để Tracee thú vị trong vùng chứa Docker:

docker run --name tracee --rm --privileged --pid=host \
  -v /lib/modules/:/lib/modules/:ro -v /usr/src/:/usr/src/ro \
  -v /tmp/tracee:/tmp/tracee aquasec/tracee:0.4.0 --trace comm=ls

# List syscalls made by all the new process on the host
docker run --name tracee --rm --privileged --pid=host \
  -v /lib/modules/:/lib/modules/:ro -v /usr/src/:/usr/src/ro \
  -v /tmp/tracee:/tmp/tracee aquasec/tracee:0.4.0 --trace pid=new

# List syscalls made from any new container
docker run --name tracee --rm --privileged --pid=host \
  -v /lib/modules/:/lib/modules/:ro -v /usr/src/:/usr/src/ro \
  -v /tmp/tracee:/tmp/tracee aquasec/tracee:0.4.0 --trace container=new

Cách hạn chế Syscalls với Seccomp

SECCOMP - Chế độ Điện toán Bảo mật - là một tính năng cấp Kernel của Linux mà bạn có thể sử dụng cho các ứng dụng hộp cát để chỉ sử dụng các cuộc gọi hệ thống mà chúng cần.

Cách kiểm tra hỗ trợ cho seccomp:

grep -i seccomp /boot/config-$(uname -r)

Cách kiểm tra để thay đổi thời gian hệ thống:

docker run -it --rm docker/whalesay /bin/sh
# date -s '19 APR 2013 22:00:00'

ps -ef

Cách kiểm tra trạng thái seccomp cho bất kỳ PID nào:

grep -i seccomp /proc/1/status

Chế độ Seccomp:

  • Chế độ 0: Đã tắt
  • Chế độ 1: Nghiêm ngặt
  • Chế độ 2: Đã lọc

Cấu hình sau được sử dụng để đưa vào danh sách trắng các cuộc gọi tổng hợp. Hồ sơ danh sách trắng được bảo mật nhưng các cuộc gọi tổng hợp phải được bật có chọn lọc vì nó chặn tất cả các cuộc gọi tổng hợp theo mặc định.

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "<syscall-1>",
        "<syscall-2>",
        "<syscall-3>"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Cấu hình sau được sử dụng để danh sách đen các cuộc gọi tổng hợp. Hồ sơ danh sách đen có bề mặt tấn công lớn hơn danh sách trắng.

{
  "defaultAction": "SCMP_ACT_ALLOW",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "<syscall-1>",
        "<syscall-2>",
        "<syscall-3>"
      ],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}

Cấu hình seccomp Docker chặn 60 trong số hơn 300 cuộc gọi tổng hợp trên kiến ​​trúc x86.

Cách sử dụng hồ sơ seccomp với Docker:

docker run -it --rm --security-opt seccomp=/root/custom.json docker/whalesay /bin/sh

Cách cho phép tất cả các cuộc gọi tổng hợp với vùng chứa:

docker run -it --rm --security-opt seccomp=unconfined docker/whalesay /bin/sh

# Verify
grep -i seccomp /proc/1/status

# Output should be:
Seccomp:         0

Cách sử dụng vùng chứa Docker để nhận thông tin liên quan đến thời gian chạy của vùng chứa:

docker run r.j3ss.co/amicontained amicontained

Seccomp trong Kubernetes

Chế độ điện toán an toàn (SECCOMP) là một tính năng của hạt nhân Linux. Bạn có thể sử dụng nó để hạn chế các tác vụ có sẵn trong vùng chứa. Tài liệu Seccomp

Cách chạy không kiểm soát trong Kubernetes:

kubectl run amicontained --image r.j3ss.co/amicontained amicontained -- amicontained

Kể từ phiên bản v1.20Kubernetes không triển khai seccomp theo mặc định.

Cấu hình docker Seccomp 'RuntimeDefault' trong Kubernetes:

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: amicontained
  name: amicontained
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - args:
    - amicontained
    image: r.j3ss.co/amicontained
    name: amicontained
    securityContext:
      allowPrivilegeEscalation: false

Vị trí seccomp mặc định trong kubelet

/var/lib/kubelet/seccomp

Cách tạo hồ sơ seccomp trong nút:

mkdir -p /var/lib/kubelet/seccomp/profiles

# Add a profile for audit
vim /var/lib/kubelet/seccomp/profiles/audit.json
>>>
{
  defaultAction: "SCMP_ACT_LOG"
}

# Add a profile for violations (Blocks all syscalls by default, will let nothing run)
vim /var/lib/kubelet/seccomp/profiles/violation.json
>>>
{
  defaultAction: "SCMP_ACT_ERRNO"
}

Hồ sơ seccomp cục bộ - tệp này phải tồn tại cục bộ trên một nút để có thể hoạt động:

...
securityContext:
  seccompProfile:
    type: Localhost
    localhostProfile: profiles/audit.json
...

Cấu hình trên sẽ cho phép các cuộc gọi tổng hợp được lưu vào một tệp.

grep syscall /var/log/syslog

Cách ánh xạ số syscall với tên syscall:

grep -w 35 /usr/include/asm/unistd_64.h

# OR
grep -w 35 /usr/include/asm-generic/unistd.h

AppArmor

AppArmor là một mô-đun bảo mật Linux được sử dụng để giới hạn một chương trình trong một nhóm tài nguyên giới hạn.

Cách cài đặt AppArmor utils:

apt-get install apparmor-utils

Cách kiểm tra xem AppArmor có đang chạy và được kích hoạt hay không:

systemctl status apparmor

cat /sys/module/apparmor/parameters/enabled
Y

Các cấu hình AppArmor được lưu trữ tại:

cat /etc/apparmor.d/root.add_data.sh

Cách liệt kê hồ sơ AppArmor:

cat /sys/kernel/security/apparmor/profiles

Cách từ chối tất cả các cấu hình ghi tệp:

profile apparmor-deny-write flags=(attach_disconnected) {
  file,
  # Deny all file writes.
  deny /** w,
}

Cách từ chối ghi vào /proc tệp:

profile apparmor-deny-proc-write flags=(attach_disconnected) {
  file,
  # Deny all file writes.
  deny /proc/* w,
}

Cách từ chối remount root FS:

profile apparmor-deny-remount-root flags=(attach_disconnected) {

  # Deny all file writes.
  deny mount options=(ro, remount) -> /,
}

Cách kiểm tra trạng thái hồ sơ:

aa-status

Chế độ tải hồ sơ

  • Enforce, giám sát và thực thi các quy tắc
  • Complain, sẽ không thực thi các quy tắc nhưng ghi lại chúng dưới dạng các sự kiện
  • Unconfined, sẽ không thực thi hoặc ghi lại các sự kiện

Cách kiểm tra xem hồ sơ có hợp lệ không:

apparmor_parser /etc/apparmor.d/root.add_data.sh

Cách tắt cấu hình:

apparmor_parser -R /etc/apparmor.d/root.add_data.sh
ln -s /etc/apparmor.d/root.add_data.sh /etc/apparmor.d/disable/

Cách tạo hồ sơ và trả lời một loạt câu hỏi sau:

aa-genprof /root/add_data.sh

Cách tạo cấu hình cho một lệnh:

aa-genprof curl

Cách tắt cấu hình khỏi nhật ký:

aa-logprof

Cách sử dụng AppArmor trong Kubernetes

Để sử dụng AppArmor với Kubernetes, bạn phải đáp ứng các điều kiện tiên quyết sau:

  • Phiên bản Kubernetes phải lớn hơn1.4
  • Mô-đun AppArmor Kernel nên được bật
  • Cấu hình AppArmor phải được tải trong hạt nhân
  • Thời gian chạy vùng chứa phải được hỗ trợ

Cách sử dụng mẫu trong Kubernetes:

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-sleeper
  annotations:
    container.apparmor.security.beta.kubernetes.io/<container-name>: localhost/<profile-name>
spec:
  containers:
  - name: ubuntu-sleeper
    image: ubuntu
    command: ["sh", "-c", "echo 'Sleeping for an hour!' && sleep 1h"]

Lưu ý : Vùng chứa phải chạy trong nút chứa cấu hình AppArmor.

Khả năng của Linux

Tính năng khả năng của Linux chia nhỏ các đặc quyền có sẵn cho các quy trình chạy khi root người dùng thành các nhóm đặc quyền nhỏ hơn. Bằng cách này, một tiến trình đang chạy với root đặc quyền có thể bị giới hạn để chỉ nhận được những quyền tối thiểu mà nó cần để thực hiện hoạt động của nó.

Docker hỗ trợ các khả năng của Linux như một phần của lệnh chạy Docker: with --cap-add--cap-drop. Theo mặc định, một vùng chứa được khởi động với một số khả năng được cho phép theo mặc định và có thể bị loại bỏ. Các quyền khác có thể được thêm theo cách thủ công.

Cả hai --cap-add--cap-drophỗ trợ giá trị TẤT CẢ, để cho phép hoặc loại bỏ tất cả các khả năng. Theo mặc định, vùng chứa Docker chạy với 14 khả năng.

  • Kernel <2,2
    • Quy trình đặc quyền
    • Quy trình đặc quyền
  • Kernel> = 2.2
    • Quy trình đặc quyền
      • CAP_CHOWN
      • CAP_SYS_TIME
      • CAP_SYS_BOOT
      • CAP_NET_ADMIN

Tham khảo tài liệu này để biết danh sách đầy đủ các Khả năng của Linux .

Cách kiểm tra những khả năng mà lệnh cần:

getcap /usr/bin/ping

Cách nhận các khả năng của quy trình:

getpcaps <pid>

Cách thêm khả năng bảo mật:

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-sleeper
spec:
  containers:
  - name: ubuntu-sleeper
    image: ubuntu
    command: ["sleep", "1000"]
    securityContext:
      capabilities:
        add: ["SYS_TIME"]
        drop: ["CHOWN"]

Làm thế nào để chuẫn bị cho một kì thi

CKS được đánh giá là một kỳ thi khá khó. Nhưng dựa trên kinh nghiệm của tôi, tôi nghĩ rằng, với thực hành đủ tốt và nếu bạn hiểu các khái niệm mà kỳ thi bao gồm, nó sẽ có thể quản lý được trong vòng hai giờ.

Bạn chắc chắn cần hiểu các khái niệm cơ bản của Kubernetes. Và vì điều kiện tiên quyết đối với CKS là phải vượt qua kỳ thi CKA, bạn nên hiểu rõ về Kubernetes và cách nó hoạt động trước khi thử CKS.

Ngoài ra, để vượt qua CKS, bạn cần hiểu các mối đe dọa và tác động bảo mật được giới thiệu bởi điều phối vùng chứa.

Sự ra đời của kỳ thi CKS là một dấu hiệu cho thấy không nên coi nhẹ an ninh của các thùng chứa. Các cơ chế bảo mật phải luôn có sẵn để ngăn chặn các cuộc tấn công vào các cụm Kubernetes.

Vụ hack tiền điện tử Tesla nhờ vào bảng điều khiển Kubernetes không được bảo vệ, làm sáng tỏ những rủi ro liên quan đến Kubernetes hoặc bất kỳ công cụ điều phối vùng chứa nào khác. Hackerone có một trang tiền thưởng Kubernetes liệt kê các kho mã nguồn được sử dụng trong một cụm Kubernetes tiêu chuẩn.

Thực hành, Thực hành và Thực hành!

Thực hành là chìa khóa để bẻ khóa kỳ thi, cá nhân tôi thấy rằng các trình mô phỏng kỳ thi của KodeKloud và Killer.sh vô cùng hữu ích đối với tôi.

Tôi không có nhiều thời gian để chuẩn bị cho kỳ thi CKS như tôi đã có cho kỳ thi CKA, nhưng tôi đang làm việc trên Kubernetes trong công việc hàng ngày của mình nên tôi thực sự cảm thấy thoải mái với nó.

Thực hành là chìa khóa thành công. Chúc bạn may mắn với kỳ thi!

Nguồn: https://www.freecodecamp.org/news/how-to-pass-the-certified-kubernetes-security-specialist-exam/

#kubernetes 

Nigel  Uys

Nigel Uys

1647411360

Column: Columnar in-Memory Store with Bitmap Indexing

Columnar In-Memory Store with Bitmap Indexing

This package contains a high-performance, columnar, in-memory storage engine that supports fast querying, update and iteration with zero-allocations and bitmap indexing.

Features

  • Optimized, cache-friendly columnar data layout that minimizes cache-misses.
  • Optimized for zero heap allocation during querying (see benchmarks below).
  • Optimized batch updates/deletes, an update during a transaction takes around 12ns.
  • Support for SIMD-enabled filtering (i.e. "where" clause) by leveraging bitmap indexing.
  • Support for columnar projection (i.e. "select" clause) for fast retrieval.
  • Support for computed indexes that are dynamically calculated based on provided predicate.
  • Support for concurrent updates using sharded latches to keep things fast.
  • Support for transaction isolation, allowing you to create transactions and commit/rollback.
  • Support for expiration of rows based on time-to-live or expiration column.
  • Support for atomic increment/decrement of numerical values, transactionally.
  • Support for change data stream that streams all commits consistently.
  • Support for concurrent snapshotting allowing to store the entire collection into a file.

Documentation

The general idea is to leverage cache-friendly ways of organizing data in structures of arrays (SoA) otherwise known "columnar" storage in database design. This, in turn allows us to iterate and filter over columns very efficiently. On top of that, this package also adds bitmap indexing to the columnar storage, allowing to build filter queries using binary and, and not, or and xor (see kelindar/bitmap with SIMD support).

Collection and Columns

In order to get data into the store, you'll need to first create a Collection by calling NewCollection() method. Each collection requires a schema, which can be either specified manually by calling CreateColumn() multiple times or automatically inferred from an object by calling CreateColumnsOf() function.

In the example below we're loading some JSON data by using json.Unmarshal() and auto-creating colums based on the first element on the loaded slice. After this is done, we can then load our data by inserting the objects one by one into the collection. This is accomplished by calling InsertObject() method on the collection itself repeatedly.

data := loadFromJson("players.json")

// Create a new columnar collection
players := column.NewCollection()
players.CreateColumnsOf(data[0])

// Insert every item from our loaded data
for _, v := range data {
    players.InsertObject(v)
}

Now, let's say we only want specific columns to be added. We can do this by calling CreateColumn() method on the collection manually to create the required columns.

// Create a new columnar collection with pre-defined columns
players := column.NewCollection()
players.CreateColumn("name", column.ForString())
players.CreateColumn("class", column.ForString())
players.CreateColumn("balance", column.ForFloat64())
players.CreateColumn("age", column.ForInt16())

// Insert every item from our loaded data
for _, v := range loadFromJson("players.json") {
    players.InsertObject(v)
}

While the previous example demonstrated how to insert many objects, it was doing it one by one and is rather inefficient. This is due to the fact that each InsertObject() call directly on the collection initiates a separate transacion and there's a small performance cost associated with it. If you want to do a bulk insert and insert many values, faster, that can be done by calling Insert() on a transaction, as demonstrated in the example below. Note that the only difference is instantiating a transaction by calling the Query() method and calling the txn.Insert() method on the transaction instead the one on the collection.

players.Query(func(txn *Txn) error {
    for _, v := range loadFromJson("players.json") {
        txn.InsertObject(v)
    }
    return nil // Commit
})

Querying and Indexing

The store allows you to query the data based on a presence of certain attributes or their values. In the example below we are querying our collection and applying a filtering operation bu using WithValue() method on the transaction. This method scans the values and checks whether a certain predicate evaluates to true. In this case, we're scanning through all of the players and looking up their class, if their class is equal to "rogue", we'll take it. At the end, we're calling Count() method that simply counts the result set.

// This query performs a full scan of "class" column
players.Query(func(txn *column.Txn) error {
    count := txn.WithValue("class", func(v interface{}) bool {
        return v == "rogue"
    }).Count()
    return nil
})

Now, what if we'll need to do this query very often? It is possible to simply create an index with the same predicate and have this computation being applied every time (a) an object is inserted into the collection and (b) an value of the dependent column is updated. Let's look at the example below, we're fist creating a rogue index which depends on "class" column. This index applies the same predicate which only returns true if a class is "rogue". We then can query this by simply calling With() method and providing the index name.

An index is essentially akin to a boolean column, so you could technically also select it's value when querying it. Now, in this example the query would be around 10-100x faster to execute as behind the scenes it uses bitmap indexing for the "rogue" index and performs a simple logical AND operation on two bitmaps when querying. This avoid the entire scanning and applying of a predicate during the Query.

// Create the index "rogue" in advance
out.CreateIndex("rogue", "class", func(v interface{}) bool {
    return v == "rogue"
})

// This returns the same result as the query before, but much faster
players.Query(func(txn *column.Txn) error {
    count := txn.With("rogue").Count()
    return nil
})

The query can be further expanded as it allows indexed intersection, difference and union operations. This allows you to ask more complex questions of a collection. In the examples below let's assume we have a bunch of indexes on the class column and we want to ask different questions.

First, let's try to merge two queries by applying a Union() operation with the method named the same. Here, we first select only rogues but then merge them together with mages, resulting in selection containing both rogues and mages.

// How many rogues and mages?
players.Query(func(txn *Txn) error {
    txn.With("rogue").Union("mage").Count()
    return nil
})

Next, let's count everyone who isn't a rogue, for that we can use a Without() method which performs a difference (i.e. binary AND NOT operation) on the collection. This will result in a count of all players in the collection except the rogues.

// How many rogues and mages?
players.Query(func(txn *Txn) error {
    txn.Without("rogue").Count()
    return nil
})

Now, you can combine all of the methods and keep building more complex queries. When querying indexed and non-indexed fields together it is important to know that as every scan will apply to only the selection, speeding up the query. So if you have a filter on a specific index that selects 50% of players and then you perform a scan on that (e.g. WithValue()), it will only scan 50% of users and hence will be 2x faster.

// How many rogues that are over 30 years old?
players.Query(func(txn *Txn) error {
    txn.With("rogue").WithFloat("age", func(v float64) bool {
        return v >= 30
    }).Count()
    return nil
})

Iterating over Results

In all of the previous examples, we've only been doing Count() operation which counts the number of elements in the result set. In this section we'll look how we can iterate over the result set.

As before, a transaction needs to be started using the Query() method on the collection. After which, we can call the txn.Range() method which allows us to iterate over the result set in the transaction. Note that it can be chained right after With..() methods, as expected.

In order to access the results of the iteration, prior to calling Range() method, we need to first load column reader(s) we are going to need, using methods such as txn.String(), txn.Float64(), etc. These prepare read/write buffers necessary to perform efficient lookups while iterating.

In the example below we select all of the rogues from our collection and print out their name by using the Range() method and accessing the "name" column using a column reader which is created by calling txn.String("name") method.

players.Query(func(txn *Txn) error {
    names := txn.String("name") // Create a column reader

    return txn.With("rogue").Range(func(i uint32) {
        name, _ := names.Get()
        println("rogue name", name)
    })
})

Similarly, if you need to access more columns, you can simply create the appropriate column reader(s) and use them as shown in the example before.

players.Query(func(txn *Txn) error {
    names := txn.String("name")
    ages  := txn.Int64("age")

    return txn.With("rogue").Range(func(i uint32) {
        name, _ := names.Get()
        age,  _ := ages.Get()

        println("rogue name", name)
        println("rogue age", age)
    })
})

Updating Values

In order to update certain items in the collection, you can simply call Range() method and use column accessor's Set() or Add() methods to update a value of a certain column atomically. The updates won't be instantly reflected given that our store supports transactions. Only when transaction is commited, then the update will be applied to the collection, allowing for isolation and rollbacks.

In the example below we're selecting all of the rogues and updating both their balance and age to certain values. The transaction returns nil, hence it will be automatically committed when Query() method returns.

players.Query(func(txn *Txn) error {
    balance := txn.Float64("balance")
    age     := txn.Int64("age")

    return txn.With("rogue").Range(func(i uint32) {
        balance.Set(10.0) // Update the "balance" to 10.0
        age.Set(50)       // Update the "age" to 50
    })
})

In certain cases, you might want to atomically increment or decrement numerical values. In order to accomplish this you can use the provided Add() operation. Note that the indexes will also be updated accordingly and the predicates re-evaluated with the most up-to-date values. In the below example we're incrementing the balance of all our rogues by 500 atomically.

players.Query(func(txn *Txn) error {
    balance := txn.Float64("balance")

    return txn.With("rogue").Range(func(i uint32) {
        balance.Add(500.0) // Increment the "balance" by 500
    })
})

Expiring Values

Sometimes, it is useful to automatically delete certain rows when you do not need them anymore. In order to do this, the library automatically adds an expire column to each new collection and starts a cleanup goroutine aynchronously that runs periodically and cleans up the expired objects. In order to set this, you can simply use InsertWithTTL() method on the collection that allows to insert an object with a time-to-live duration defined.

In the example below we are inserting an object to the collection and setting the time-to-live to 5 seconds from the current time. After this time, the object will be automatically evicted from the collection and its space can be reclaimed.

players.InsertObjectWithTTL(map[string]interface{}{
    "name": "Merlin",
    "class": "mage",
    "age": 55,
    "balance": 500,
}, 5 * time.Second) // The time-to-live of 5 seconds

On an interesting note, since expire column which is automatically added to each collection is an actual normal column, you can query and even update it. In the example below we query and conditionally update the expiration column. The example loads a time, adds one hour and updates it, but in practice if you want to do it you should use Add() method which can perform this atomically.

players.Query(func(txn *column.Txn) error {
    expire := txn.Int64("expire")

    return txn.Range(func(i uint32) {
        if v, ok := expire.Get(); ok && v > 0 {
            oldExpire := time.Unix(0, v) // Convert expiration to time.Time
            newExpire := expireAt.Add(1 * time.Hour).UnixNano()  // Add some time
            expire.Set(newExpire)
        }
    })
})

Transaction Commit and Rollback

Transactions allow for isolation between two concurrent operations. In fact, all of the batch queries must go through a transaction in this library. The Query method requires a function which takes in a column.Txn pointer which contains various helper methods that support querying. In the example below we're trying to iterate over all of the players and update their balance by setting it to 10.0. The Query method automatically calls txn.Commit() if the function returns without any error. On the flip side, if the provided function returns an error, the query will automatically call txn.Rollback() so none of the changes will be applied.

// Range over all of the players and update (successfully their balance)
players.Query(func(txn *column.Txn) error {
    balance := txn.Float64("balance")
    txn.Range(func(i uint32) {
        v.Set(10.0) // Update the "balance" to 10.0
    })

    // No error, transaction will be committed
    return nil
})

Now, in this example, we try to update balance but a query callback returns an error, in which case none of the updates will be actually reflected in the underlying collection.

// Range over all of the players and update (successfully their balance)
players.Query(func(txn *column.Txn) error {
    balance := txn.Float64("balance")
    txn.Range(func(i uint32) {
        v.Set(10.0) // Update the "balance" to 10.0
    })

    // Returns an error, transaction will be rolled back
    return fmt.Errorf("bug")
})

Streaming Changes

This library also supports streaming out all transaction commits consistently, as they happen. This allows you to implement your own change data capture (CDC) listeners, stream data into kafka or into a remote database for durability. In order to enable it, you can simply provide an implementation of a commit.Writer interface during the creation of the collection.

In the example below we take advantage of the commit.Channel implementation of a commit.Writer which simply publishes the commits into a go channel. Here we create a buffered channel and keep consuming the commits with a separate goroutine, allowing us to view transactions as they happen in the store.

// Create a new commit writer (simple channel) and a new collection
writer  := make(commit.Channel, 1024)
players := NewCollection(column.Options{
    Writer: writer,
})

// Read the changes from the channel
go func(){
    for commit := writer{
        println("commit", commit.ID)
    }
}()

// ... insert, update or delete

On a separate note, this change stream is guaranteed to be consistent and serialized. This means that you can also replicate those changes on another database and synchronize both. In fact, this library also provides Replay() method on the collection that allows to do just that. In the example below we create two collections primary and replica and asychronously replicating all of the commits from the primary to the replica using the Replay() method together with the change stream.

// Create a p rimary collection
writer  := make(commit.Channel, 1024)
primary := column.NewCollection(column.Options{
    Writer: &writer,
})
primary.CreateColumnsOf(object)

// Replica with the same schema
replica := column.NewCollection()
replica.CreateColumnsOf(object)

// Keep 2 collections in sync
go func() {
    for change := range writer {
        replica.Replay(change)
    }
}()

Snapshot and Restore

The collection can also be saved in a single binary format while the transactions are running. This can allow you to periodically schedule backups or make sure all of the data is persisted when your application terminates.

In order to take a snapshot, you must first create a valid io.Writer destination and then call the Snapshot() method on the collection in order to create a snapshot, as demonstrated in the example below.

dst, err := os.Create("snapshot.bin")
if err != nil {
    panic(err)
}

// Write a snapshot into the dst
err := players.Snapshot(dst)

Conversely, in order to restore an existing snapshot, you need to first open an io.Reader and then call the Restore() method on the collection. Note that the collection and its schema must be already initialized, as our snapshots do not carry this information within themselves.

src, err := os.Open("snapshot.bin")
if err != nil {
    panic(err)
}

// Restore from an existing snapshot
err := players.Restore(src)

Complete Example

func main(){

    // Create a new columnar collection
    players := column.NewCollection()
    players.CreateColumn("serial", column.ForKey())
    players.CreateColumn("name", column.ForEnum())
    players.CreateColumn("active", column.ForBool())
    players.CreateColumn("class", column.ForEnum())
    players.CreateColumn("race", column.ForEnum())
    players.CreateColumn("age", column.ForFloat64())
    players.CreateColumn("hp", column.ForFloat64())
    players.CreateColumn("mp", column.ForFloat64())
    players.CreateColumn("balance", column.ForFloat64())
    players.CreateColumn("gender", column.ForEnum())
    players.CreateColumn("guild", column.ForEnum())

    // index on humans
    players.CreateIndex("human", "race", func(r column.Reader) bool {
        return r.String() == "human"
    })

    // index for mages
    players.CreateIndex("mage", "class", func(r column.Reader) bool {
        return r.String() == "mage"
    })

    // index for old
    players.CreateIndex("old", "age", func(r column.Reader) bool {
        return r.Float() >= 30
    })

    // Load the items into the collection
    loaded := loadFixture("players.json")
    players.Query(func(txn *column.Txn) error {
        for _, v := range loaded {
            txn.InsertObject(v)
        }
        return nil
    })

    // Run an indexed query
    players.Query(func(txn *column.Txn) error {
        name := txn.Enum("name")
        return txn.With("human", "mage", "old").Range(func(idx uint32) {
            value, _ := name.Get()
            println("old mage, human:", value)
        })
    })
}

Benchmarks

The benchmarks below were ran on a collection of 100,000 items containing a dozen columns. Feel free to explore the benchmarks but I strongly recommend testing it on your actual dataset.

cpu: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz
BenchmarkCollection/insert-8            2523     469481 ns/op    24356 B/op    500 allocs/op
BenchmarkCollection/select-at-8     22194190      54.23 ns/op        0 B/op      0 allocs/op
BenchmarkCollection/scan-8              2068     568953 ns/op      122 B/op      0 allocs/op
BenchmarkCollection/count-8           571449       2057 ns/op        0 B/op      0 allocs/op
BenchmarkCollection/range-8            28660      41695 ns/op        3 B/op      0 allocs/op
BenchmarkCollection/update-at-8      5911978      202.8 ns/op        0 B/op      0 allocs/op
BenchmarkCollection/update-all-8        1280     946272 ns/op     3726 B/op      0 allocs/op
BenchmarkCollection/delete-at-8      6405852      188.9 ns/op        0 B/op      0 allocs/op
BenchmarkCollection/delete-all-8     2073188      562.6 ns/op        0 B/op      0 allocs/op

When testing for larger collections, I added a small example (see examples folder) and ran it with 20 million rows inserted, each entry has 12 columns and 4 indexes that need to be calculated, and a few queries and scans around them.

running insert of 20000000 rows...
-> insert took 20.4538183s

running snapshot of 20000000 rows...
-> snapshot took 2.57960038s

running full scan of age >= 30...
-> result = 10200000
-> full scan took 61.611822ms

running full scan of class == "rogue"...
-> result = 7160000
-> full scan took 81.389954ms

running indexed query of human mages...
-> result = 1360000
-> indexed query took 608.51µs

running indexed query of human female mages...
-> result = 640000
-> indexed query took 794.49µs

running update of balance of everyone...
-> updated 20000000 rows
-> update took 214.182216ms

running update of age of mages...
-> updated 6040000 rows
-> update took 81.292378ms

Contributing

We are open to contributions, feel free to submit a pull request and we'll review it as quickly as we can. This library is maintained by Roman Atachiants

Author: Kelindar
Source Code: https://github.com/kelindar/column 
License: MIT License

#go #golang 

Column: Columnar in-Memory Store with Bitmap Indexing

Columnar In-Memory Store with Bitmap Indexing

This package contains a high-performance, columnar, in-memory storage engine that supports fast querying, update and iteration with zero-allocations and bitmap indexing.

Features

  • Optimized, cache-friendly columnar data layout that minimizes cache-misses.
  • Optimized for zero heap allocation during querying (see benchmarks below).
  • Optimized batch updates/deletes, an update during a transaction takes around 12ns.
  • Support for SIMD-enabled aggregate functions such as "sum", "avg", "min" and "max".
  • Support for SIMD-enabled filtering (i.e. "where" clause) by leveraging bitmap indexing.
  • Support for columnar projection (i.e. "select" clause) for fast retrieval.
  • Support for computed indexes that are dynamically calculated based on provided predicate.
  • Support for concurrent updates using sharded latches to keep things fast.
  • Support for transaction isolation, allowing you to create transactions and commit/rollback.
  • Support for expiration of rows based on time-to-live or expiration column.
  • Support for atomic increment/decrement of numerical values, transactionally.
  • Support for primary keys for use-cases where offset can't be used.
  • Support for change data stream that streams all commits consistently.
  • Support for concurrent snapshotting allowing to store the entire collection into a file.

Documentation

The general idea is to leverage cache-friendly ways of organizing data in structures of arrays (SoA) otherwise known "columnar" storage in database design. This, in turn allows us to iterate and filter over columns very efficiently. On top of that, this package also adds bitmap indexing to the columnar storage, allowing to build filter queries using binary and, and not, or and xor (see kelindar/bitmap with SIMD support).

Collection and Columns

In order to get data into the store, you'll need to first create a Collection by calling NewCollection() method. Each collection requires a schema, which can be either specified manually by calling CreateColumn() multiple times or automatically inferred from an object by calling CreateColumnsOf() function.

In the example below we're loading some JSON data by using json.Unmarshal() and auto-creating colums based on the first element on the loaded slice. After this is done, we can then load our data by inserting the objects one by one into the collection. This is accomplished by calling InsertObject() method on the collection itself repeatedly.

data := loadFromJson("players.json")

// Create a new columnar collection
players := column.NewCollection()
players.CreateColumnsOf(data[0])

// Insert every item from our loaded data
for _, v := range data {
    players.InsertObject(v)
}

Now, let's say we only want specific columns to be added. We can do this by calling CreateColumn() method on the collection manually to create the required columns.

// Create a new columnar collection with pre-defined columns
players := column.NewCollection()
players.CreateColumn("name", column.ForString())
players.CreateColumn("class", column.ForString())
players.CreateColumn("balance", column.ForFloat64())
players.CreateColumn("age", column.ForInt16())

// Insert every item from our loaded data
for _, v := range loadFromJson("players.json") {
    players.InsertObject(v)
}

While the previous example demonstrated how to insert many objects, it was doing it one by one and is rather inefficient. This is due to the fact that each InsertObject() call directly on the collection initiates a separate transacion and there's a small performance cost associated with it. If you want to do a bulk insert and insert many values, faster, that can be done by calling Insert() on a transaction, as demonstrated in the example below. Note that the only difference is instantiating a transaction by calling the Query() method and calling the txn.Insert() method on the transaction instead the one on the collection.

players.Query(func(txn *Txn) error {
    for _, v := range loadFromJson("players.json") {
        txn.InsertObject(v)
    }
    return nil // Commit
})

Querying and Indexing

The store allows you to query the data based on a presence of certain attributes or their values. In the example below we are querying our collection and applying a filtering operation bu using WithValue() method on the transaction. This method scans the values and checks whether a certain predicate evaluates to true. In this case, we're scanning through all of the players and looking up their class, if their class is equal to "rogue", we'll take it. At the end, we're calling Count() method that simply counts the result set.

// This query performs a full scan of "class" column
players.Query(func(txn *column.Txn) error {
    count := txn.WithValue("class", func(v interface{}) bool {
        return v == "rogue"
    }).Count()
    return nil
})

Now, what if we'll need to do this query very often? It is possible to simply create an index with the same predicate and have this computation being applied every time (a) an object is inserted into the collection and (b) an value of the dependent column is updated. Let's look at the example below, we're fist creating a rogue index which depends on "class" column. This index applies the same predicate which only returns true if a class is "rogue". We then can query this by simply calling With() method and providing the index name.

An index is essentially akin to a boolean column, so you could technically also select it's value when querying it. Now, in this example the query would be around 10-100x faster to execute as behind the scenes it uses bitmap indexing for the "rogue" index and performs a simple logical AND operation on two bitmaps when querying. This avoid the entire scanning and applying of a predicate during the Query.

// Create the index "rogue" in advance
out.CreateIndex("rogue", "class", func(v interface{}) bool {
    return v == "rogue"
})

// This returns the same result as the query before, but much faster
players.Query(func(txn *column.Txn) error {
    count := txn.With("rogue").Count()
    return nil
})

The query can be further expanded as it allows indexed intersection, difference and union operations. This allows you to ask more complex questions of a collection. In the examples below let's assume we have a bunch of indexes on the class column and we want to ask different questions.

First, let's try to merge two queries by applying a Union() operation with the method named the same. Here, we first select only rogues but then merge them together with mages, resulting in selection containing both rogues and mages.

// How many rogues and mages?
players.Query(func(txn *Txn) error {
    txn.With("rogue").Union("mage").Count()
    return nil
})

Next, let's count everyone who isn't a rogue, for that we can use a Without() method which performs a difference (i.e. binary AND NOT operation) on the collection. This will result in a count of all players in the collection except the rogues.

// How many rogues and mages?
players.Query(func(txn *Txn) error {
    txn.Without("rogue").Count()
    return nil
})

Now, you can combine all of the methods and keep building more complex queries. When querying indexed and non-indexed fields together it is important to know that as every scan will apply to only the selection, speeding up the query. So if you have a filter on a specific index that selects 50% of players and then you perform a scan on that (e.g. WithValue()), it will only scan 50% of users and hence will be 2x faster.

// How many rogues that are over 30 years old?
players.Query(func(txn *Txn) error {
    txn.With("rogue").WithFloat("age", func(v float64) bool {
        return v >= 30
    }).Count()
    return nil
})

Iterating over Results

In all of the previous examples, we've only been doing Count() operation which counts the number of elements in the result set. In this section we'll look how we can iterate over the result set.

As before, a transaction needs to be started using the Query() method on the collection. After which, we can call the txn.Range() method which allows us to iterate over the result set in the transaction. Note that it can be chained right after With..() methods, as expected.

In order to access the results of the iteration, prior to calling Range() method, we need to first load column reader(s) we are going to need, using methods such as txn.String(), txn.Float64(), etc. These prepare read/write buffers necessary to perform efficient lookups while iterating.

In the example below we select all of the rogues from our collection and print out their name by using the Range() method and accessing the "name" column using a column reader which is created by calling txn.String("name") method.

players.Query(func(txn *Txn) error {
    names := txn.String("name") // Create a column reader

    return txn.With("rogue").Range(func(i uint32) {
        name, _ := names.Get()
        println("rogue name", name)
    })
})

Similarly, if you need to access more columns, you can simply create the appropriate column reader(s) and use them as shown in the example before.

players.Query(func(txn *Txn) error {
    names := txn.String("name")
    ages  := txn.Int64("age")

    return txn.With("rogue").Range(func(i uint32) {
        name, _ := names.Get()
        age,  _ := ages.Get()

        println("rogue name", name)
        println("rogue age", age)
    })
})

Taking the Sum() of a (numeric) column reader will take into account a transaction's current filtering index.

players.Query(func(txn *Txn) error {
    totalAge := txn.With("rouge").Int64("age").Sum()
    totalRouges := int64(txn.Count())

    avgAge := totalAge / totalRouges

    txn.WithInt("age", func(v float64) bool {
        return v < avgAge
    })

    // get total balance for 'all rouges younger than the average rouge'
    balance := txn.Float64("balance").Sum()
    return nil
})

Updating Values

In order to update certain items in the collection, you can simply call Range() method and use column accessor's Set() or Add() methods to update a value of a certain column atomically. The updates won't be instantly reflected given that our store supports transactions. Only when transaction is commited, then the update will be applied to the collection, allowing for isolation and rollbacks.

In the example below we're selecting all of the rogues and updating both their balance and age to certain values. The transaction returns nil, hence it will be automatically committed when Query() method returns.

players.Query(func(txn *Txn) error {
    balance := txn.Float64("balance")
    age     := txn.Int64("age")

    return txn.With("rogue").Range(func(i uint32) {
        balance.Set(10.0) // Update the "balance" to 10.0
        age.Set(50)       // Update the "age" to 50
    })
})

In certain cases, you might want to atomically increment or decrement numerical values. In order to accomplish this you can use the provided Add() operation. Note that the indexes will also be updated accordingly and the predicates re-evaluated with the most up-to-date values. In the below example we're incrementing the balance of all our rogues by 500 atomically.

players.Query(func(txn *Txn) error {
    balance := txn.Float64("balance")

    return txn.With("rogue").Range(func(i uint32) {
        balance.Add(500.0) // Increment the "balance" by 500
    })
})

Expiring Values

Sometimes, it is useful to automatically delete certain rows when you do not need them anymore. In order to do this, the library automatically adds an expire column to each new collection and starts a cleanup goroutine aynchronously that runs periodically and cleans up the expired objects. In order to set this, you can simply use Insert...() method on the collection that allows to insert an object with a time-to-live duration defined.

In the example below we are inserting an object to the collection and setting the time-to-live to 5 seconds from the current time. After this time, the object will be automatically evicted from the collection and its space can be reclaimed.

players.Insert(func(r column.Row) error {
    r.SetString("name", "Merlin")
    r.SetString("class", "mage")
    r.SetTTL(5 * time.Second) // time-to-live of 5 seconds
    return nil
})

On an interesting note, since expire column which is automatically added to each collection is an actual normal column, you can query and even update it. In the example below we query and extend the time-to-live by 1 hour using the Extend() method.

players.Query(func(txn *column.Txn) error {
    ttl := txn.TTL()
    return txn.Range(func(i uint32) {
        ttl.Extend(1 * time.Hour) // Add some time
    })
})

Transaction Commit and Rollback

Transactions allow for isolation between two concurrent operations. In fact, all of the batch queries must go through a transaction in this library. The Query method requires a function which takes in a column.Txn pointer which contains various helper methods that support querying. In the example below we're trying to iterate over all of the players and update their balance by setting it to 10.0. The Query method automatically calls txn.Commit() if the function returns without any error. On the flip side, if the provided function returns an error, the query will automatically call txn.Rollback() so none of the changes will be applied.

// Range over all of the players and update (successfully their balance)
players.Query(func(txn *column.Txn) error {
    balance := txn.Float64("balance")
    txn.Range(func(i uint32) {
        v.Set(10.0) // Update the "balance" to 10.0
    })

    // No error, transaction will be committed
    return nil
})

Now, in this example, we try to update balance but a query callback returns an error, in which case none of the updates will be actually reflected in the underlying collection.

// Range over all of the players and update (successfully their balance)
players.Query(func(txn *column.Txn) error {
    balance := txn.Float64("balance")
    txn.Range(func(i uint32) {
        v.Set(10.0) // Update the "balance" to 10.0
    })

    // Returns an error, transaction will be rolled back
    return fmt.Errorf("bug")
})

Using Primary Keys

In certain cases it is useful to access a specific row by its primary key instead of an index which is generated internally by the collection. For such use-cases, the library provides Key column type that enables a seamless lookup by a user-defined primary key. In the example below we create a collection with a primary key name using CreateColumn() method with a ForKey() column type. Then, we use InsertKey() method to insert a value.

players := column.NewCollection()
players.CreateColumn("name", column.ForKey())     // Create a "name" as a primary-key
players.CreateColumn("class", column.ForString()) // .. and some other columns

// Insert a player with "merlin" as its primary key
players.InsertKey("merlin", func(r column.Row) error {
    r.SetString("class", "mage")
    return nil
})

Similarly, you can use primary key to query that data directly, without knowing the exact offset. Do note that using primary keys will have an overhead, as it requires an additional step of looking up the offset using a hash table managed internally.

// Query merlin's class
players.QueryKey("merlin", func(r column.Row) error {
    class, _ := r.String("class")
    return nil
})

Streaming Changes

This library also supports streaming out all transaction commits consistently, as they happen. This allows you to implement your own change data capture (CDC) listeners, stream data into kafka or into a remote database for durability. In order to enable it, you can simply provide an implementation of a commit.Logger interface during the creation of the collection.

In the example below we take advantage of the commit.Channel implementation of a commit.Logger which simply publishes the commits into a go channel. Here we create a buffered channel and keep consuming the commits with a separate goroutine, allowing us to view transactions as they happen in the store.

// Create a new commit writer (simple channel) and a new collection
writer  := make(commit.Channel, 1024)
players := NewCollection(column.Options{
    Writer: writer,
})

// Read the changes from the channel
go func(){
    for commit := range writer {
        fmt.Printf("commit %v\n", commit.ID)
    }
}()

// ... insert, update or delete

On a separate note, this change stream is guaranteed to be consistent and serialized. This means that you can also replicate those changes on another database and synchronize both. In fact, this library also provides Replay() method on the collection that allows to do just that. In the example below we create two collections primary and replica and asychronously replicating all of the commits from the primary to the replica using the Replay() method together with the change stream.

// Create a primary collection
writer  := make(commit.Channel, 1024)
primary := column.NewCollection(column.Options{
    Writer: &writer,
})
primary.CreateColumnsOf(object)

// Replica with the same schema
replica := column.NewCollection()
replica.CreateColumnsOf(object)

// Keep 2 collections in sync
go func() {
    for change := range writer {
        replica.Replay(change)
    }
}()

Snapshot and Restore

The collection can also be saved in a single binary format while the transactions are running. This can allow you to periodically schedule backups or make sure all of the data is persisted when your application terminates.

In order to take a snapshot, you must first create a valid io.Writer destination and then call the Snapshot() method on the collection in order to create a snapshot, as demonstrated in the example below.

dst, err := os.Create("snapshot.bin")
if err != nil {
    panic(err)
}

// Write a snapshot into the dst
err := players.Snapshot(dst)

Conversely, in order to restore an existing snapshot, you need to first open an io.Reader and then call the Restore() method on the collection. Note that the collection and its schema must be already initialized, as our snapshots do not carry this information within themselves.

src, err := os.Open("snapshot.bin")
if err != nil {
    panic(err)
}

// Restore from an existing snapshot
err := players.Restore(src)

Complete Example

func main(){

    // Create a new columnar collection
    players := column.NewCollection()
    players.CreateColumn("serial", column.ForKey())
    players.CreateColumn("name", column.ForEnum())
    players.CreateColumn("active", column.ForBool())
    players.CreateColumn("class", column.ForEnum())
    players.CreateColumn("race", column.ForEnum())
    players.CreateColumn("age", column.ForFloat64())
    players.CreateColumn("hp", column.ForFloat64())
    players.CreateColumn("mp", column.ForFloat64())
    players.CreateColumn("balance", column.ForFloat64())
    players.CreateColumn("gender", column.ForEnum())
    players.CreateColumn("guild", column.ForEnum())

    // index on humans
    players.CreateIndex("human", "race", func(r column.Reader) bool {
        return r.String() == "human"
    })

    // index for mages
    players.CreateIndex("mage", "class", func(r column.Reader) bool {
        return r.String() == "mage"
    })

    // index for old
    players.CreateIndex("old", "age", func(r column.Reader) bool {
        return r.Float() >= 30
    })

    // Load the items into the collection
    loaded := loadFixture("players.json")
    players.Query(func(txn *column.Txn) error {
        for _, v := range loaded {
            txn.InsertObject(v)
        }
        return nil
    })

    // Run an indexed query
    players.Query(func(txn *column.Txn) error {
        name := txn.Enum("name")
        return txn.With("human", "mage", "old").Range(func(idx uint32) {
            value, _ := name.Get()
            println("old mage, human:", value)
        })
    })
}

Benchmarks

The benchmarks below were ran on a collection of 100,000 items containing a dozen columns. Feel free to explore the benchmarks but I strongly recommend testing it on your actual dataset.

cpu: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz
BenchmarkCollection/insert-8            2523     469481 ns/op    24356 B/op    500 allocs/op
BenchmarkCollection/select-at-8     22194190      54.23 ns/op        0 B/op      0 allocs/op
BenchmarkCollection/scan-8              2068     568953 ns/op      122 B/op      0 allocs/op
BenchmarkCollection/count-8           571449       2057 ns/op        0 B/op      0 allocs/op
BenchmarkCollection/range-8            28660      41695 ns/op        3 B/op      0 allocs/op
BenchmarkCollection/update-at-8      5911978      202.8 ns/op        0 B/op      0 allocs/op
BenchmarkCollection/update-all-8        1280     946272 ns/op     3726 B/op      0 allocs/op
BenchmarkCollection/delete-at-8      6405852      188.9 ns/op        0 B/op      0 allocs/op
BenchmarkCollection/delete-all-8     2073188      562.6 ns/op        0 B/op      0 allocs/op

When testing for larger collections, I added a small example (see examples folder) and ran it with 20 million rows inserted, each entry has 12 columns and 4 indexes that need to be calculated, and a few queries and scans around them.

running insert of 20000000 rows...
-> insert took 20.4538183s

running snapshot of 20000000 rows...
-> snapshot took 2.57960038s

running full scan of age >= 30...
-> result = 10200000
-> full scan took 61.611822ms

running full scan of class == "rogue"...
-> result = 7160000
-> full scan took 81.389954ms

running indexed query of human mages...
-> result = 1360000
-> indexed query took 608.51µs

running indexed query of human female mages...
-> result = 640000
-> indexed query took 794.49µs

running update of balance of everyone...
-> updated 20000000 rows
-> update took 214.182216ms

running update of age of mages...
-> updated 6040000 rows
-> update took 81.292378ms

Contributing

We are open to contributions, feel free to submit a pull request and we'll review it as quickly as we can. This library is maintained by Roman Atachiants

Download Details:

Author: Kelindar
Source Code: https://github.com/kelindar/column 
License: MIT license

#go #golang #db #data 

Dexter  Goodwin

Dexter Goodwin

1657216500

Level-hash-index: Store any String into Level, & Get A Collision Free

What it does

Store any string into level, and get a collision free hash of that value that you can use in an index or something.

For example if you put in the values hello, world, some very long string, hello, hello, and some very long string. What ends up being stored in level is this:

KeyValue
23a4hello
r2d2world
d59csome very long string

As you noticed, although some values were put in multiple times, they are only stored once. Each value is given a unique, collision free hash.

Why is this a good idea?

When making a query engine of some kind you often want to create indexes that stores the same values in several different arrangements. So instead of storing the full values multiple times in the indexes, you can instead just store the hashes, thus keeping the indexes from ballooning up in size.

How to use it

var HashIndex = require('level-hash-index');
var h = HashIndex(db);// db is anything that exposes the levelUp API

h.putAndWrite("hello", function(err, hash){
  // hash === "23a4..."
  // ...
  // ...
  h.getHash(hash, function(err, val){
    // val === "hello"
  });
});

h = HashIndex(db, options)

  • db is your levelup db (i.e. level, sublevel, multilevel etc... )
  • options.hashFn any hash function that takes in a string and returns a string
  • options.index_prefix the level key = index_prefix + hash (default prefix is "hash!")

h.put(val, callback(err, data))

Given a value, get it's unique hash. The callback data is one of these two

  • {hash: hash} - the hash of the value that's persisted to the db
  • {hash: hash, is_new: true, key: db_key} - the hash of the value and it's raw db key that hasn't yet been persisted, but is the hash that will be used once it is persisted.

h.putAndWrite(val, callback(err, hash))

Given a value, get it's hash, and write to the db if it hasn't yet been persisted.

h.getHash(val, callback(err, hash))

Given a value, get the hash for that value, if it has been persisted to the db. Uses level's NotFoundError if not found.

h.get(hash, callback(err, val))

Given a hash get the value. Uses level's NotFoundError if not found.

Author: Smallhelm
Source Code: https://github.com/smallhelm/level-hash-index 
License: MIT license

#javascript #index #stores #hash