2021-07-30

Mock vs Fake and Classical Testing

Motivation of this article is to promote less painful way of testing, structuring codes, and less broken test when changing logic/implementation details (only changing the logic not changing the input/output). This post recapping past ~4 years compilation of articles that conclude that Fake > Mock, Classical Test > Mock Test from other developers that realize the similar pain points of popular approach (mock).

Mock Approach

Given a code like this:

type Obj struct {
*sql.DB // or Provider
}
func (o *Obj) DoMultipleQuery(in InputStruct) (out OutputStruct, err error) {
... = o.DoSomeQuery()
... = o.DoOtherQuery()
}

I’ve seen code to test with mock technique like this:

func TestObjDoMultipleQuery(t *testing.T) {
o := Obj{mockProvider{}}
testCases := []struct {
name string
mockFunc func(sqlmock.Sqlmock, *gomock.Controller) in InputStruct out OutputStruct
shouldErr bool
} {
{
name: `bast case`,
mockFunc: func(db sqlmock.SqlMock, c *gomock.Controller) {
db.ExpectExec(`UPDATE t1 SET bla = \?, foo = \?, yay = \? WHERE bar = \? LIMIT 1`).
WillReturnResult(sqlmock.NewResult(1,1))
db.ExpectQuery(`SELECT a, b, c, d, bar, bla, yay FROM t1 WHERE bar = \? AND state IN \(1,2\)`).
WithArgs(3).
WillReturnRows(sqlmock.NewRows([]string{"id", "channel_name", "display_name", "color", "description", "active", "updated_at"}).
AddRow("2", "bla2", "Bla2", "#0000", "bla bla", "1", "2021-05-18T15:04:05Z").
AddRow("3", "wkwk", "WkWk", "#0000", "wkwk", "1", "2021-05-18T15:04:05Z"))
...
}, in: InputStruct{...}, out: OutputStruct{...},
wantErr: false,
},
{
... other cases
},
} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T){ ... // prepare mock object o := Obj{mockProvider} out := o.DoMultipleQueryBusinessLogic(tc.in) assert.Equal(t, out, tc.out) }) }
}

This approach has pros and cons:

+ could check whether has typos (eg. add one character in the original query, this test would detect the error)

+ could check whether some queries are properly called, or not called but expected to be called

+ unit test would always faster than integration test

- testing implementation detail (easily break when the logic changed)

- cannot check whether the SQL statements are correct

- possible coupled implementation between data provider and business logic

- duplicating work between original query and the regex-version of query, which if add a column, we must change both implementation

For the last cons, we can change it to something like this:

db.ExpectQuery(`SELECT.+FROM t1.+`).
WillReturnRows( ... )

This approach has pros and cons:

+ not deduplicating works (since it just a simplified regex of the full SQL statements

+ still can check whether queries properly called or not

+ unit test would always faster than integration test

- testing implementation detail (easily break when the logic changed)

- cannot detect typos/whether the query no longer match (eg. if we accidentally add one character on the original query that can cause sql error)

- cannot check correctness of the SQL statement

- possible coupled implementation between data provider and business logic

We could also create a helper function to replace the original query to regex version:

func SqlToRegexSql(sql string) string {
return // replace special characters in regex (, ), ?, . with escaped version
}
db.ExpectQuery(SqlToRegexSql(ORIGINAL_QUERY)) ...

This approach has same pros and cons as previous approach.

Fake Approach

Fake testing use classical approach, instead of checking implementation detail (expected calls to dependency), we use a compatible implementation as dependency (eg. a slice/map of struct for database table/DataProvider)

Given a code like this:

type Obj struct {
FooDataProvider // interface{UpdateFoo,GetFoo,...}
}
func (o *Obj) DoBusinessLogic(in *Input) (out *Output,err error) {
... = o.UpdateFoo(in.bla)
... = o.GetFoo(in.bla)
...
}

It’s better to make a fake data provider like this:

type FakeFooDataProvider struct {
Rows map[int]FooRow{} // or slice
}
func (f *FakeFooDataProvider) UpdateFoo(a string) (...) { /* update Rows */}
func (f *FakeFooDataProvider) GetFoo(a string) (...) { /* get one Rows */}
... // insert, delete, count, get batched/paged

So in the test, we can do something like this:

func TestObjDoBusinessLogic(t *testing.T) {
o := Obj{FakeFooDataProvider{}}
testCases := []struct{
name string
in Input
out Ouput
shouldErr bool
} {
{
name: `best case`,
in: Input{...},
out: Output{...},
shouldErr: false,
},
{
...
},
} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T){ out := o.DoBusinessLogic(tc.in) assert.Equal(t, tc.out, out) }) }
}

This approach have pros and cons:

+ testing behavior (this input should give this output) instead of implementation detail (not easily break/no need to modify the test when algorithm/logic changed)

+ unit test would always faster than integration test

- cannot check whether the queries are called or not called but expected to be called

- double work in Golang (since there’s no generic/template yet, go 1.18 must wait Feb 2022), must create minimal fake implementation (map/slice) that simulate basic database table logic, or if data provider not separated between tables (repository/entity pattern) must create a join logic too – better approach in this case is to always create Insert, Update, Delete, GetOne, GetBatch instead of joining.

+ should be no coupling between queries and business logic

Cannot check whether queries in data provider are correct (which should not be the problem of this unit, it should be DataProvider integration/unit test’s problem, not this unit)

Classical Approach for DataProvider

It’s better to test the queries using classical (black box) approach integration test instead of mock (white box), since mock and fake testing can only test the correctness of business logic, not logic of the data provider that mostly depend to a 2nd party (database). Fake testing also considered a classical approach, since it test input/output not implementation detail.

Using dockertest when test on local and gitlab-ci service when test on pipeline, can be something like this:

var testDbConn *sql.DB
func TestMain(m *testing.M) int { // called before test
if env == `` || env == `development` { // spawn dockertest, return connection to dockertest
prepareDb(func(db *sql.DB){
testDbConn = db
if db == nil {
return 0
}
return m.Run()
})
} else {
// connect to gitlab-ci service var err error testDbConn, err = ... // log error
}
}
func TestDataProviderLogic(t *testing.T) {
if testDbConn == nil {
if env == `` || env == `development` || env == `test` {
t.Fail()
}
return
}
f := FooDataProvider{testDbConn}
f.InitTables()
f.MigrateTables() // if testing migration
// test f.UpdateFoo, f.GetFoo, ...
}

Where the prepareDb function can be something like this (taken from dockertest example):

func prepareDb(onReady func(db *sql.DB) int) {
const dockerRepo = `yandex/clickhouse-server`
const dockerVer = `latest`
const chPort = `9000/tcp`
const dbDriver = "clickhouse"
const dbConnStr = "tcp://127.0.0.1:%s?debug=true"
var err error
if globalPool == nil {
globalPool, err = dockertest.NewPool("")
if err != nil {
log.Printf("Could not connect to docker: %s\n", err)
return
}
}
resource, err := globalPool.Run(dockerRepo, dockerVer, []string{})
if err != nil {
log.Printf("Could not start resource: %s\n", err)
return
}
var db *sql.DB
if err := globalPool.Retry(func() error {
var err error
db, err = sql.Open(dbDriver, fmt.Sprintf(dbConnStr, resource.GetPort(chPort)))
if err != nil {
return err
}
return db.Ping()
}); err != nil {
log.Printf("Could not connect to docker: %s\n", err)
return
}
code := onReady(db)
if err := globalPool.Purge(resource); err != nil {
log.Fatalf("Could not purge resource: %s", err)
}
os.Exit(code)
}

In the pipeline the .gitlab-ci.yml file can be something like this for PostgreSQL (use tmpfs/inmem version for database data directory to make it faster):

test:
stage: test
image: golang:1.16.4
dependencies: []
services:
- postgres:13-alpine # TODO: create a tmpfs version
tags:
- cicd
variables:
ENV: test
POSTGRES_DB: postgres
POSTGRES_HOST: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_PORT: "5432"
POSTGRES_USER: postgres
script:
- source env.sample
- go test

The dockerfile for tmpfs database if using MySQL can be something like this:

FROM circleci/mysql:5.5

RUN echo '\n\
[mysqld]\n\
datadir = /dev/inmemory/mysql\n\
' >> /etc/mysql/my.cnf

Or for MongoDB:

FROM circleci/mongo:3.6.9

RUN sed -i '/exec "$@"/i mkdir \/dev\/inmemory\/mongo' /usr/local/bin/docker-entrypoint.sh

CMD ["mongod", "--nojournal", "--noprealloc", "--smallfiles", "--dbpath=/dev/inmemory/mongo"]

The benefit of this classical integration test approach:

+ high confidence that your SQL statements are correct, can detect typos (wrong column, wrong table, etc)

+ isolated test, not testing business logic but only data provider layer, also can test for schema migrations

- not a good approach for database with eventual consistency (eg. Clickhouse)

- since this is an integration test,  it would be slower than mock/fake unit test (1-3s+ total delay overhead when spawning docker)

Conclusion

  1. use mock for databases with eventual consistency

  2. prefer fake over mock for business logic correctness because it’s better for maintainability to test behavior (this input should give this output), instead of implementation details

  3. prefer classical testing over mock testing for checking data provider logic correctness

References

(aka confirmation bias :3)

https://martinfowler.com/articles/mocksArentStubs.html
https://stackoverflow.com/questions/1595166/why-is-it-so-bad-to-mock-classes
https://medium.com/javascript-scene/mocking-is-a-code-smell-944a70c90a6a
https://chemaclass.medium.com/to-mock-or-not-to-mock-af995072b22e
https://accu.org/journals/overload/23/127/balaam_2108/
https://news.ycombinator.com/item?id=7809402
https://philippe.bourgau.net/careless-mocking-considered-harmful/
https://debugged.it/blog/mockito-is-bad-for-your-code/
https://engineering.talkdesk.com/double-trouble-why-we-decided-against-mocking-498c915bbe1c
https://blog.thecodewhisperer.com/permalink/you-dont-hate-mocks-you-hate-side-effects
https://agilewarrior.wordpress.com/2015/04/18/classical-vs-mockist-testing/
https://www.slideshare.net/davidvoelkel/mockist-vs-classicists-tdd-57218553
https://www.thoughtworks.com/insights/blog/mockists-are-dead-long-live-classicists
https://stackoverflow.com/questions/184666/should-i-practice-mockist-or-classical-tdd
https://bencane.com/2020/06/15/dont-mock-a-db-use-docker-compose/
https://swizec.com/blog/what-i-learned-from-software-engineering-at-google/#stubs-and-mocks-make-bad-tests

https://www.freecodecamp.org/news/end-to-end-api-testing-with-docker/
https://medium.com/@june.pravin/mocking-is-not-practical-use-fakes-e30cc6eaaf4e 
https://www.c-sharpcorner.com/article/stub-vs-fake-vs-spy-vs-mock/

2021-07-08

Prime Benchmark

So, yesterday I didn't touch my PC to do a prime number benchmark. And Here's the result for single threaded (only showing fastest implementation of each language) and multithreaded:

IndexImplementationSLabelPassesDurAlgoFaithfulBitPasses/Second
1cpp3flo80_2206675.00baseno144133.26760
10c2daniel202505.00wheelyes14049.85259
11zig3ManDeJ179645.00baseno13592.49823
12c2daniel176815.00wheelyes13536.07129
16rust1mike-b158045.01baseyes83152.68929
20assembly1rberge144345.00baseno82886.80000
22haskell1fatho/119595.00baseno82391.77321
32fortran1johand99875.00baseno11997.40000
36crystal1marghi86805.00baseyes11735.86981
38fsharp3dmanno77545.00baseyes1550.68897
40java1Mansen1488710.0baseyes1488.70000
41csharp1kinema72715.00baseyes1454.08077
43julia2epithe69535.00baseyes11390.55577
46go2ssoves61615.00baseyes11232.01471
51nodejs1rogier57485.00baseyes11149.43213
57lisp2mayerr51225.00baseno11024.19803
58typescript1marghi50315.00baseyes1006.20000
59d2Bradle50035.00baseyes11000.52396
61v1marghi43295.00baseyes865.80000
63lua2ben1je31595.00baseno1631.80000
64nim2beef3328715.00baseyes1574.02096
67cython1rpkak26595.00baseyes8531.64832
71basic1rberge24165.00wheelyes1483.00680
73assemblyscript1donmah423110.0baseyes423.05768
74python2ssoves19915.00baseyes8398.09742
80scala1rom1de12035.00baseyes240.55189
81pascal1rberge11625.00baseyes232.40000
82cobol1fvbake11575.00baseno8231.40000
83pony1marghi11445.00baseyes1228.80000
84swift1j-f1204610.0baseyes204.55332
85dart1eagere8245.00baseyes164.77795
86haxe1TayIor139210.0baseyes139.19035
88ada1BoopBe6615.00baseno132.02220
92octave1octave3135.00baseno62.54234
93postscript1epithe2165.01baseno843.08797
94ruby1rberge1195.01baseyes23.71935
95wren1marghi1115.00baseyes22.16446
96php1Dennis14310.0baseyes14.24667
97smalltalk1fvbake495.07baseyes19.66469
99mixal1rberge304.91baseno16.10998
100perl1marghi285.16baseyes5.42031
103r1fvbake75.43baseyes321.28842
104sql2fvbake65.43otherno321.10375
105tcl1fvbake65.47baseyes11.09589
111latex1tjol217.8baseno320.11224
112bash1bash110.6baseno0.09357

IndexImplementationSLabelPassesDurThreadAlgoFaithfulBitPasses/Second
1zig3ManDe1409105.04wheelno17045.26046
2cpp3flo802361845.08baseno15904.57992
3zig3ManDe1063995.04wheelno15319.64146
4zig3ManDe1010265.04wheelno15051.08785
5zig3ManDe840025.04wheelno14200.08320
6zig3ManDe1478225.08wheelno13695.38740
7zig3ManDe720215.04wheelno13600.73314
8zig3ManDe705225.04wheelno13525.83204
9zig3ManDe1341245.08wheelno13352.91894
10zig3ManDe598515.04baseno82992.45424
11c2danie1013915.08wheelyes12534.43285
12zig3ManDe982465.08wheelno12455.98299
13zig3ManDe981905.08wheelno12454.57327
14zig3ManDe481645.04baseno12408.10849
15zig3ManDe917455.08wheelno12293.50574
16zig3ManDe905985.08wheelno12264.84129
17c2danie881035.08wheelyes12199.97727
18zig3ManDe423185.04baseno12115.85768
19c2danie788585.08wheelyes11969.05838
20zig3ManDe684925.08baseno81712.11852
21c2danie637525.08wheelyes11591.86334
22rust1mike-590015.08baseyes81474.83765
23rust1mike-529795.08baseyes11324.32205
24c2danie498225.08baseyes11244.43126
25zig3ManDe497125.08baseno11242.59124
26c2danie246645.04wheelyes11233.15067
27rust1mike-492385.08baseyes11230.78145
28zig3ManDe456365.08baseno11140.80189
29c2danie223785.04wheelyes11118.88478
30c2danie203855.04wheelyes11019.22065
31c2danie202575.04wheelyes11012.82346
32c2danie151325.04baseyes1756.55294
33cpp2davep200485.08baseyes1501.18496
34d2Bradl195535.08baseyes1488.74973
35zig3ManDe92405.04baseno8461.94272
36zig3ManDe118345.08baseno8295.81332
37csharp1kinem31935.08wheelyes179.81766
38csharp1kinem29615.08baseyes174.02041

Raw result on this gist

2021-05-31

Easy Tarantool and ClickHouse Replication Setup

These two are currently my favorite databases because of their speed and features, Tarantool (200K tps) for OLTP, and ClickHouse (600K insert/s, 100 qps) for OLAP. Today we will learn how to setup a multi-master replication on each database:

# docker-compose.yml # tarantool
version: '3.3'
services:
  tt1: # master
    image: tarantool/tarantool:2.7.2 # x.x.2+ = stable
    volumes:
      - ./scripts/:/opt/scripts
    command: tarantool /opt/scripts/app.lua
    environment:
      - TARANTOOL_USER_NAME=tester
      - TARANTOOL_USER_PASSWORD=tester
    ports:
      - 13301:3301
  tt2: # slave1
    image: tarantool/tarantool:2.7.2
    volumes:
      - ./scripts/:/opt/scripts
    command: tarantool /opt/scripts/app.lua
    environment:
      - TARANTOOL_USER_NAME=tester
      - TARANTOOL_USER_PASSWORD=tester
    ports:
      - 23301:3301
  tt3: # slave2
    image: tarantool/tarantool:2.7.2
    volumes:
      - ./scripts/:/opt/scripts
    command: tarantool /opt/scripts/app.lua
    environment:
      - TARANTOOL_USER_NAME=tester
      - TARANTOOL_USER_PASSWORD=tester
    ports:
      - 33301:3301

# scripts/app.lua # need to be set on different port/file if not using docker-compose
# for multi-master, you should not use counter data type or it would be out of sync/conflict
# so it's better to use master-slave (2 read_only replica)
box.cfg{
    listen = 3301,
    replication = {
        'replicator:password@tt1:3301', -- master URI
        'replicator:password@tt2:3301', -- replica 1 URI
        'replicator:password@tt3:3301', -- replica 2 URI
    },
    read_only = false # set to true for replica 1 and 2 if you want master-slave
}
box.once("schema", function()
    box.schema.user.create('replicator', {password = 'password'})
    box.schema.user.grant('replicator', 'replication') -- grant replication role
    box.schema.space.create("test")
    box.space.test:create_index("primary")
    print('box.once executed on master')
end)

# start it
docker-compose up

# create table and insert on master
tarantoolctl connect tester:tester@127.0.0.1:13301
connected to 127.0.0.1:13301

127.0.0.1:13301> box.execute [[ create table test1(id int primary key, name string) ]]
---
- row_count: 1
...

127.0.0.1:13301> box.execute [[ insert into test1(id,name) values(1,'test') ]]
---
- row_count: 1
...

# check on slave cluster node
tarantoolctl connect tester:tester@127.0.0.1:23301
connected to 127.0.0.1:23301
127.0.0.1:23301> box.execute [[ select * FROM test1 ]]
---
- metadata:
  - name: ID
    type: integer
  - name: NAME
    type: string
  rows:
  - [1, 'test']
...



That's it, very easy right? Now for the ClickHouse

# docker-compose.yml # clickhouse
version: '3.3'
services:  ch1:
    image: yandex/clickhouse-server
    restart: always
    volumes:
      - ./config.xml:/etc/clickhouse-server/config.d/local.xml
      - ./macro1.xml:/etc/clickhouse-server/config.d/macros.xml
      - ./data/1:/var/lib/clickhouse    
    ports:
      - '18123:8123'
      - '19000:9000'
      - '19009:9009'
    ulimits:
      nproc: 65536
      nofile:
        soft: 252144
        hard: 252144
  ch2:
    image: yandex/clickhouse-server
    restart: always
    volumes:
      - ./config.xml:/etc/clickhouse-server/config.d/local.xml
      - ./macro2.xml:/etc/clickhouse-server/config.d/macros.xml
      - ./data/2:/var/lib/clickhouse
    ports:
      - '28123:8123'
      - '29000:9000'
      - '29009:9009'
    ulimits:
      nproc: 65536
      nofile:
        soft: 252144
        hard: 252144
  ch3:
    image: yandex/clickhouse-server
    restart: always
    volumes:
      - ./config.xml:/etc/clickhouse-server/config.d/local.xml
      - ./macro3.xml:/etc/clickhouse-server/config.d/macros.xml
      - ./data/3:/var/lib/clickhouse
    ports:
      - '38123:8123'
      - '39000:9000'
      - '39009:9009'
    ulimits:
      nproc: 65536
      nofile:
        soft: 252144
        hard: 252144
  zookeeper:
    image: zookeeper

# config.xml
<yandex>
    <remote_servers>
        <replicated>
            <shard>
                <internal_replication>true</internal_replication>
                <replica>
                    <host>ch1</host>
                    <port>9000</port>
                </replica>
                <replica>
                    <host>ch2</host>
                    <port>9000</port>
                </replica>
                <replica>
                    <host>ch3</host>
                    <port>9000</port>
                </replica>
            </shard>
        </replicated>
    </remote_servers>
    <zookeeper>
        <node>
            <host>zookeeper</host>
            <port>2181</port>
        </node>
    </zookeeper>
</yandex>

# macroXX.xml # replace XX with 1, 2, or 3
<yandex>
    <macros replace="replace">
        <cluster>cluster1</cluster>
        <replica>chXX</replica>
    </macros>
</yandex>

# start it
docker-compose up

# create table and insert on first cluster node
clickhouse-client --port 19000
SELECT * FROM system.clusters;
CREATE DATABASE db1 ON CLUSTER replicated;
SHOW DATABASES;
USE db1;

CREATE TABLE IF NOT EXISTS db1.table1 ON CLUSTER replicated
( id UInt64
, dt Date
, val UInt64
) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/{cluster}/tables/table1',
'{replica}')
PARTITION BY modulo( id, 1000 )
ORDER BY (dt);

INSERT INTO db1.
table1
(id, dt, val)
VALUES (1,'2021-05-31',2);

# check on second cluster node
clickhouse-client --port 29000
SELECT * FROM db1.table1;

┌─id─┬─dt
─┬─val─┐
│  1 │ 2021-05-31 │   2 │
└────┴────────────┴─────┘
↘ Progress: 1.00 rows, 42.00 B (132.02 rows/s., 5.54 KB/s.)  99%
1 rows in set. Elapsed: 0.008 sec.


That's it, you now can have both database on single computer to start testing the replication or for development (it's recommended to downscale it just to single replica tho).

2021-05-27

Benchmarking Disks on VPS: SSD, NVMe or HDD

Are you sure you are getting SSD when you rent a VPS? Here's how to make sure

sudo apt install fio
fio --randrepeat=1 --ioengine=libaio --direct=1 --gtod_reduce=1 --name=test --filename=random_read_write.fio --bs=4k --iodepth=64 --size=4G --readwrite=randrw --rwmixread=75 ; rm random_read_write.fio

The result would be something like this:

SSD:

test: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.16
Starting 1 process
test: Laying out IO file (1 file / 4096MiB)
Jobs: 1 (f=1): [m(1)][100.0%][r=83.7MiB/s,w=27.3MiB/s][r=21.4k,w=6993 IOPS][eta 00m:00s]
test: (groupid=0, jobs=1): err= 0: pid=1436574: Thu May 27 19:01:27 2021
  read: IOPS=18.2k, BW=70.0MiB/s (74.4MB/s)(3070MiB/43241msec)
   bw (  KiB/s): min=  456, max=130000, per=99.85%, avg=72589.30, stdev=36572.29, samples=86
   iops        : min=  114, max=32500, avg=18147.33, stdev=9143.07, samples=86
  write: IOPS=6074, BW=23.7MiB/s (24.9MB/s)(1026MiB/43241msec); 0 zone resets
   bw (  KiB/s): min=  176, max=42936, per=99.85%, avg=24259.07, stdev=12200.49, samples=86
   iops        : min=   44, max=10734, avg=6064.77, stdev=3050.12, samples=86
  cpu          : usr=3.35%, sys=13.20%, ctx=781969, majf=0, minf=10
  IO depths    : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
     issued rwts: total=785920,262656,0,0 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
   READ: bw=70.0MiB/s (74.4MB/s), 70.0MiB/s-70.0MiB/s (74.4MB/s-74.4MB/s), io=3070MiB (3219MB), run=43241-43241msec
  WRITE: bw=23.7MiB/s (24.9MB/s), 23.7MiB/s-23.7MiB/s (24.9MB/s-24.9MB/s), io=1026MiB (1076MB), run=43241-43241msec

Disk stats (read/write):
  sdh: ios=782456/263158, merge=1293/2586, ticks=1838928/822350, in_queue=2134502, util=99.12%

HDD:

too slow (1MB/s would took about 40 minutes)

VPS:

test: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.16
Starting 1 process
test: Laying out IO file (1 file / 4096MiB)
Jobs: 1 (f=1): [m(1)][100.0%][r=11.2MiB/s,w=3924KiB/s][r=2873,w=981 IOPS][eta 00m:00s]
test: (groupid=0, jobs=1): err= 0: pid=3829977: Thu May 27 20:04:10 2021
  read: IOPS=3029, BW=11.8MiB/s (12.4MB/s)(3070MiB/259388msec)
   bw (  KiB/s): min= 7744, max=144776, per=99.95%, avg=12112.97, stdev=7631.07, samples=518
   iops        : min= 1936, max=36194, avg=3028.16, stdev=1907.77, samples=518
  write: IOPS=1012, BW=4050KiB/s (4148kB/s)(1026MiB/259388msec); 0 zone resets
   bw (  KiB/s): min= 2844, max=47936, per=99.94%, avg=4047.41, stdev=2504.77, samples=518
   iops        : min=  711, max=11984, avg=1011.83, stdev=626.19, samples=518
  cpu          : usr=2.89%, sys=10.00%, ctx=605914, majf=0, minf=8
  IO depths    : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
     issued rwts: total=785920,262656,0,0 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
   READ: bw=11.8MiB/s (12.4MB/s), 11.8MiB/s-11.8MiB/s (12.4MB/s-12.4MB/s), io=3070MiB (3219MB), run=259388-259388msec
  WRITE: bw=4050KiB/s (4148kB/s), 4050KiB/s-4050KiB/s (4148kB/s-4148kB/s), io=1026MiB (1076MB), run=259388-259388msec

Disk stats (read/write):
  sda: ios=785732/271244, merge=0/973, ticks=795112/15862499, in_queue=15747316, util=99.75
 

 Conclusion

If your VPS speed more than HDD on table below, it's a big possibility that it's using SSD, or at least RAID. But there's another possibility that they are throttling shared VPS so it wouldn't distrub other people's QoS.

 Update

comparison with local NVMe it would be something like this:

   read: IOPS=61.6k, BW=241MiB/s (252MB/s)(3070MiB/12752msec)
  write: IOPS=20.6k, BW=80.5MiB/s (84.4MB/s)(1026MiB/12752msec); 0 zone resets
   read: IOPS=40.2k, BW=157MiB/s (165MB/s)(3070MiB/19546msec)
  write: IOPS=13.4k, BW=52.5MiB/s (55.0MB/s)(1026MiB/19546msec); 0 zone resets

comparison with local HDD with bad sectors it would be something like this:
 
   read: IOPS=217, BW=868KiB/s (889kB/s)(23.6MiB/27852msec)
  write: IOPS=74, BW=296KiB/s (304kB/s)(8256KiB/27852msec); 0 zone resets
 
comparison with vps that claimed to be SSD it would be something like this:
 
   read: IOPS=2908, BW=11.4MiB/s (11.9MB/s)(289MiB/25446msec)
  write: IOPS=965, BW=3861KiB/s (3954kB/s)(95.9MiB/25446msec); 0 zone resets

   read: IOPS=3182, BW=12.4MiB/s (13.0MB/s)(1728MiB/139002msec)
  write: IOPS=1066, BW=4268KiB/s (4370kB/s)(579MiB/139002msec); 0 zone resets


2021-03-30

Kubernetes IDE/GUI

There's various GUI for Kubernetes that I've found:
For now my recommendation would be Kontenta-Lens, you'll get a bird-view of your cluster.
For shell autocomplete, I recommend kube-prompt (or other shell-specific autocomplete)

If you only need docker GUI, you can try Dry
If you prefer web-based GUI, you can try Portainer (it could manage much more than just kubernetes, but also local docker and docker swarm), it's quite better than Rancher.

2021-03-13

Pyroscope: Continuous Tracing in Go, Python, or Ruby

Recently I stumbled upon slow library/function problem and don't know chich part that causes it, and found out that there's a easy way to trace either Go, Ruby, or Python code using Pyroscope. The feature is a bit minimalist, there's no memory usage tracing yet unlike in gops or pprof. Pyroscope consist 2 parts: the server and the agent/client library (if using Golang) or executor (if using Ruby or Python). Here's the way how to run and start Pyroscope server:

# run server using docker
docker run -it -p 4040:4040 pyroscope/pyroscope:latest server

And here's the example on how to use the client library/agent (modifying Go's source code, just like in DataDog or any other APM tools) and install  the Pyroscope CLI to run Ruby/Python scripts:

# golang, add agent inside the source code
import "github.com/pyroscope-io/pyroscope/pkg/agent/profiler"
func main() {
  profiler.Start(profiler.Config{
    ApplicationName: "my.app.server", 
    ServerAddress:   "http://pyroscope:4040",
  })
  // rest of your code
}

# ruby or python, install CLI client 
cd /tmp
wget https://dl.pyroscope.io/release/pyroscope_0.0.28_amd64.deb
sudo apt-get install ./pyroscope_0.0.28_amd64.deb

# ruby
pyroscope exec ruby yourcode.rb

# python
pyroscope exec python yourcode.py

It would show something like this if you open the server URL (localhost:4040) in the browser, so you can check which part of the code that took most of the runtime.




2021-01-26

GOPS: Trace your Golang service with ease

GoPS is one alternative (also made by Google, other than pprof) to measure, trace or diagnose the performance and memory usage your Go-powered service/long-lived program. The usage is very easy, you just need to import and add 3 lines in your main (so the gops command line can communicate with your program):

import "github.com/google/gops/agent"

func main() {
  if err := agent.Listen(agent.Options{}); err != nil {
    log.Fatal(err)
  }
  // remaining of your long-lived program logic
}

If you don't put those lines, you can still use gops limited to get list of programs running on your computer/server that made with Go with limited statistics information, using these commands:

$ go get -u -v github.com/google/gops

$ gops  # show the list of running golang program
1248    1       dnscrypt-proxy  go1.13.4  /usr/bin/dnscrypt-proxy
1259    1       containerd      go1.13.15 /usr/bin/containerd
18220   1       dockerd         go1.13.15 /usr/bin/dockerd
1342132 1306434 docker          go1.13.15 /usr/bin/docker

$ gops tree # show running process in tree

$ gops PID # check the stats and whether the program have GOPS agent

#########################################################
# these commands below only available
# if the binary compiled with GOPS agent
# PID can be replaced with GOPS host:port of that program

$ gops stack PID # get current stack trace of running PID

$ gops memstats PID # get memory statistics of running PID

$ gops gc PID # force garbage collection

$ gops setgc PID X # set GC percentage

$ gops pprof-cpu PID # get cpu profile graph
$ gops pprof-heap PID # get memory usage profile graph
profile saved at /tmp/heap_profile070676630
$ gops trace PID # get 5 sec execution trace

# you can install graphviz to visualize the cpu/memory profile
$ sudo apt install graphviz

# visualize the cpu/memory profile graph on the web browser
$ go tool pprof /tmp/heap_profile070676630
> web 

Next step is analyze the call graph for the memory leaks (which mostly just wrongly/forgot to defer body/sql rows or holding slice reference of huge buffer or certain framework's cache trashing) or slow functions, whichever your mission are.

What if golang service you need to trace it inside Kubernetes pod that the GOPS address (host:port) not exposed to outside-world? Kubernetes is a popular solution for companies that manages bunch of servers/microservices or cloud like (GKE, AKS, Amazon EKS, ACK, DOKS, etc) but obviously overkill solution for small companies that doesn't need to scale elastically (or the servers are less than 10 or not using microservice architecture).

First, you must compile gops statically so it can be run inside alpine container (which mostly what people use):

$ cd $GOPATH/go/src/github.com/google/gops
$ export CGO_ENABLED=0
$ go mod vendor
$ go build

# copy gops to your kubernetes pod
$ export POD_NAME=blabla
$ kubectl cp ./gops $POD_NAME:/bin

# ssh/exec to your pod
$ kubectl exec -it $POD_NAME -- sh
$ gops

# for example you want to check heap profile for PID=1
$ gops pprof-heap 1
$ exit

# copy back trace file to local, then you can analyze the dump
kubectl cp $POD:/tmp/heap_profile070676630 out.dump

But if your address and port are exposed you can directly use gops from your computer to the pod or create a tunnel inside the pod if it doesn't have public IP, for example using ngrok.

Btw if you know any companies migrating from/to certain language (especially Go), frameworks or database, you can contribute here.

2021-01-15

Learn Python in X minutes

Too bad that Python become increasingly more popular now, the fastest implementation (PyPy which nearly as fast as Dart > Java and TCC > Crystal and NodeJS > Nim > Go) somehow not fully compatible with the default implementation (CPython which slower than CRuby > Lua > PHP >> LuaJIT and V > PyPy). Haven't really learned about Python (and Lua) in depth (last time I learned it is about 8 years ago, same as RubyOnRails), since most of my projects can be covered with Go (backend), Ruby (scripting), Javascript (frontend), C# (games). Probably the biggest motivation to learn Python is data science (there's a bunch of libraries binding), also for LuaJIT is possibly the fastest embeddable language (FFI) that could bind to C easily compared to another language and bunch of game engines that uses Lua (also have you heard Core? it's Unity3D like game engine, that have low visibility). But this article is not about Lua, so now we'll try to learn Python, the most used stuff only (one that used in competitive programming):

Variable and data types (int, long, string, float, bool) and structures (list, tuple, dict, set):

v1 = 1 # int or long (bigint) data type
help(v1) # list of methods and documentations
v1 = 'str' # string data type
dir(v1) # list of methods as array of string
v1 = 1.2 # float
print(v1.__doc__) # get documentation of current type or method
type(v1) == float

v1 = [1,2,'test'] # list data type
v1[-1] == 'test' # True (bool data type)
v1[::2] == [1,'test'] # step each 2
v1[:2] == [1,2] # from beginning until before index 2
v1[1:] == [2,'test'] # from index 1 until the end
range(3) == [0,1,2] # generate from range
range(1,7,2) == [1,3,5] # jump every 2
del v1[1] # delete at pos 1
any([True,1,2,3]) == True
any(['',0,False]) == False
sum([1,2,3]) == 6
v1 = [1,2,3]
v1.append(4) # changes v1 to [1,2,3,4]
v2 = v1[:] # deep copy
v1.pop() # 4
v1.remove(3) # remove first occurence
v1.insert(1,99) # changes v1 to [1,99,2]
v1.index(99) # first 99 found at position 1
v1 + v2 # [1,99,2,1,2,3,4]
v1.extend(v2) # updates v1 to ^
len(v1) # 7
i = iter(v1)
next(i) # 1
v1 = [3,2,1]
v1.sort() # v1 now [1,2,3]

v1 = (1,'test') # tuple data type
tuple([1,2,'test']) == (1,2,'test') # convert to tuple
list((1,2,'test')) == [1,2,'test'] # convert to list
v1[0] == 1 # can be indexed, but readonly
v1 += (2,) # (1,'test',2)

v1 = {'a':1, 2:3} # dict data type
list(v1) == v1.keys()
v1.values() == [1,3]
v1[4] = 5 
3 in v1 # False, because no key 3 in v1
v1.get(99) == None 
del v1['a']

v1 = set()
v1 = {1,2,2,1,1,3} # {1,2,3}
v1.add(3) # no effect
v1 & {1,2} # intersection
v1 | {3,4} # union
v1 - {1,2} # difference
v1 ^ {1,4} # symetric difference
v1 >= {1,2} # True if right all included in v1
v1.copy() # deep copy

Operators (arithmetic, comparison, logical, assignment, bitwise, identity/is, membership/in):

v1 / 2 # float division
v1 // 2 # integer division
v1 ** 3 # exponentiation
not (v1 > 3) # bool
v1 != 2 and v1 < 5 # bool
v1 != 2 or v1 < 5 # bool
v1 is 3 # only true when referring to same object
3 is 3 # variable with const always false
'test' + '123' # concat


v1 = '%s %d %.2f' % ('yay',1,1.2345)
v1 == 'yay 1 1.23'

v1 = '%(name)s age is %(age).2f' % {'name':'kis', 'age':34.5}
v1 == 'kis age is 34.50'

v1 = """
multi line {}
""".format('string')
v1 == '\nmulti line string\n'

'%4s' % 'a' == '   a' # 3 space before a, no need for tuple if one

int('2') == 2
float('2.34') == 2.34
str(123.456) == '123.456'
bool(0) # False, also False for '', [], {}, ()
print('yay',end='') # without newline

Control structure (if, for, while):

if 's' in v1:
  print('inside')
elif '\n' in v1:
  print('have newline')
else:
  print('not having s or newline')

for v in v1: # only key if dict, can be iter
  print(v) # can use continue or break
else:
  print('only called when no break')

while True:
  break

# list comprehension
[x * x for x in [1,2,3]] == [1,4,9]
[x * y for x in [1,2,3] for y in [10,20]] == [10,20,30,40,50,60]

# dict comprehension
{x:1 for x in 'abcdef' if x not in 'acf'} == {'b':1,'d':1,'e':1}

Function and lambda:

d = 0

def f1(a,b=2,c='c'):
  global d # if need to modify global variable
  d += 1
  print(a,b,c,d) # print as tuple

f1([],3) # ([],3,'c',1)

f1 = lambda(y): y + 'test'
print(f1('a')) # atest

filter(lambda(x): x%2 == 0, [1,2,3,4,5]) == [2,4]

def f2(*a):
  print(a)

f2(1,2,'a') # (1,2,'a')

def f3(**a):
  print(a)

f3(x=1,y=2) # {'x':1,'y':2}

v1 = ['a','foo','ca']
v1.sort(key=lambda(x): len(x)) # sort by length

def f4(x):
  return -len(x)

v1.sort(key=f4) # without lambda

Class and object:

class Human(object):
  static_var = 10
  def __init__(self):
    self.name = 'kis'
  def set_age(self,a):
    self.age = a
  def get_both(self):
    return (self.name,self.age)
  def static_method():
    return 'whoa'

h = Human()
h.set_age(34)
print(h.get_both())
h.address = 'dzf 1/23' # create new member
del h.address 

Human.static_var = 3 # all object points to this new value
h.static_var = 4 # only this object points to this new value
h2 = Human()
h2.static_var == 3 # last time changed from 10 to 3

Importing libraries:

import random
from time import clock
random.seed(clock())
v1 = random.randint(1,10) # inclusive 1 to 10
print(v1)

import math
math.sqrt(9) # 3.0

from math import ceil, floor
ceil(3.4) # 4.0
floor(3.9) # 3.0

import math as m
m.sqrt(4) # 2.0

from heapq import heappush, heappop, heapify
v1 = []
heappush(v1,4)
heappush(v1,6)
heappush(v1,5)
heappop(v1) # 4
heappop(v1) # 5
heappop(v1) # 6
v1 = [(4,'a'),(2,'b'),(1,'c'),(3,'d')]
heapify(v1) # heappop() 4 times will return sorted order

from bisect import bisect_left # C++'s lower_bound
from collections import deque
v1 = deque(v1)
v1.appendleft((1,'c'))
bisect_left(v1,(2,'b')) == 1 # returns nearest if not exists

class KV(object):
  def __init__(self, l, key):
    self.l = l
    self.key = key
  def __len__(self): # overrides len()
    return len(self.l)
  def __getitem__(self, index): # overrides []
    return self.key(self.l[idx])
  def get(self, idx):
    return l[idx]

v1 = 
KV(v1,lambda(x): x[0])
bisect_left(v1,3)

I think that's for now, I exclude try-except-else-finally (or catch all exception) and making a module in this tutorial.