New FAMILUG

The PyMiers

Tuesday, 24 September 2024

[go] Viết test code prometheus xem delta và rate tính thế nào

Bài trước đã dùng mắt để chạy code funcDeltafuncRate của Prometheus để xem code chạy qua những dòng nào, những phép tính nào được tính trong extrapolatedRate. Bài này sẽ sử dụng go test coverage để xem code nào được chạy.

Viết test Prometheus

Code của funcDeltafuncRate đều nằm trong file promql/functions.go, theo quy ước chung của Go, test sẽ nằm trong promql/functions_test.go, nhưng file này chỉ chứa có 2 test:

$ grep ^func -c functions*_test.go
functions_internal_test.go:1
functions_test.go:2

Lý do là PromQL tự tạo ra 1 kiểu/ngôn ngữ test riêng cho ngôn ngữ PromQL, nằm trong thư mục promqltest/ chứ không phải Golang test thông thường. file promqltest/README.md viết:

The PromQL test scripting language

This package contains two things:

  • an implementation of a test scripting language for PromQL engines
  • a predefined set of tests written in that scripting language
$ find promql/promqltest
promql/promqltest
promql/promqltest/README.md
promql/promqltest/testdata
promql/promqltest/testdata/aggregators.test
promql/promqltest/testdata/at_modifier.test
promql/promqltest/testdata/collision.test
promql/promqltest/testdata/literals.test
promql/promqltest/testdata/selectors.test
promql/promqltest/testdata/staleness.test
promql/promqltest/testdata/trig_functions.test
promql/promqltest/testdata/range_queries.test
promql/promqltest/testdata/functions.test
promql/promqltest/testdata/native_histograms.test
promql/promqltest/testdata/operators.test
promql/promqltest/testdata/limit.test
promql/promqltest/testdata/histograms.test
promql/promqltest/testdata/subquery.test
promql/promqltest/test.go
promql/promqltest/test_test.go

may thay, trong functions_test vẫn có 1 function, và 1 là đủ để làm ví dụ rồi:

func TestDeriv(t *testing.T) {
    // https://github.com/prometheus/prometheus/issues/2674#issuecomment-315439393
    // This requires more precision than the usual test system offers,
    // so we test it by hand.

Copy function này, tạo function để test funcDelta:

func TestDelta(t *testing.T) {
    storage := teststorage.New(t)
    defer storage.Close()
    opts := promql.EngineOpts{
        Logger:     nil,
        Reg:        nil,
        MaxSamples: 10000,
        Timeout:    10 * time.Second,
    }
    engine := promql.NewEngine(opts)

    a := storage.Appender(context.Background())

    metric := labels.FromStrings("__name__", "foo")

    a.Append(0, metric, 1726745659174, 120)
    a.Append(0, metric, 1726745674174, 122)
    a.Append(0, metric, 1726745689174, 134)
    a.Append(0, metric, 1726745704174, 149)

    require.NoError(t, a.Commit())

    ctx := context.Background()
    query, err := engine.NewInstantQuery(ctx, storage, nil, "delta(foo[1m])", timestamp.Time(1726745705174))
    require.NoError(t, err)

    result := query.Exec(ctx)
    require.NoError(t, result.Err)

    vec, _ := result.Vector()
    require.Len(t, vec, 1, "Expected 1 result, got %d", len(vec))
    require.Equal(t, 0.0, vec[0].F, "Expected 0.0 as value, got %f", vec[0].F)
}

Chạy test tạo coverprofile

$ go test -run TestDelta -coverprofile=coverage.delta
--- FAIL: TestDelta (0.01s)
    functions_test.go:61:
            Error Trace:    ~/prometheus/promql/functions_test.go:61
            Error:          Not equal:
                            expected: 0
                            actual  : 38.666666666666664
            Test:           TestDelta
            Messages:       Expected 0.0 as value, got 38.666667
FAIL
coverage: 14.5% of statements
exit status 1
FAIL    github.com/prometheus/prometheus/promql 0.023s
$ go tool cover -html=coverage.delta

File này chỉ là 1 file text với nội dung

mode: set
github.com/prometheus/prometheus/promql/engine.go:86.41,88.2 1 0
github.com/prometheus/prometheus/promql/engine.go:102.41,104.2 1 0
github.com/prometheus/prometheus/promql/engine.go:106.42,108.2 1 0
github.com/prometheus/prometheus/promql/engine.go:110.43,112.2 1 0
...
github.com/prometheus/prometheus/promql/functions.go:61.116,65.2 1 0
github.com/prometheus/prometheus/promql/functions.go:71.148,89.60 5 1
github.com/prometheus/prometheus/promql/functions.go:89.60,91.3 1 0
...

mở trình duyệt với hình: delta

Tương tự để test rate:

func TestRate(t *testing.T) {
    storage := teststorage.New(t)
    defer storage.Close()
    opts := promql.EngineOpts{
        Logger:     nil,
        Reg:        nil,
        MaxSamples: 10000,
        Timeout:    10 * time.Second,
    }
    engine := promql.NewEngine(opts)

    a := storage.Appender(context.Background())

    metric := labels.FromStrings("__name__", "foo")

    a.Append(0, metric, 1726745659174, 120)
    a.Append(0, metric, 1726745674174, 122)
    a.Append(0, metric, 1726745689174, 134)
    a.Append(0, metric, 1726745704174, 149)

    require.NoError(t, a.Commit())

    ctx := context.Background()
    query, err := engine.NewInstantQuery(ctx, storage, nil, "rate(foo[1m])", timestamp.Time(1726745705174))
    require.NoError(t, err)

    result := query.Exec(ctx)
    require.NoError(t, result.Err)

    vec, _ := result.Vector()
    require.Len(t, vec, 1, "Expected 1 result, got %d", len(vec))
    require.Equal(t, 0.0, vec[0].F, "Expected 0.0 as value, got %f", vec[0].F)
}

Chạy

$ go test -run TestRate -coverprofile=coverage.rate
--- FAIL: TestRate (0.01s)
    functions_test.go:61:
            Error Trace:    /home/hvn/code/prometheus/promql/functions_test.go:61
            Error:          Not equal:
                            expected: 0
                            actual  : 0.6444444444444444
            Test:           TestRate
            Messages:       Expected 0.0 as value, got 0.644444
FAIL
coverage: 14.9% of statements
exit status 1
FAIL    github.com/prometheus/prometheus/promql 0.021s
$ go tool cover -html=coverage.rate

rate

Kết luận

Prometheus function không tính sai, nó chỉ tính đúng như trong tài liệu mô tả. Hãy đọc tài liệu. Hoặc đọc code.

Hết.

HVN at http://pymi.vn and https://www.familug.org.

Ủng hộ đồng bào bị ảnh hưởng bởi cơn bão số 3.

Báo Tuổi Trẻ, Ngân hàng Công thương chi nhánh 3, TP.HCM. Số tài khoản: 113000006100 (Việt Nam đồng). Nội dung: Ủng hộ đồng bào bị ảnh hưởng bởi cơn bão số 3.

yagi

Thursday, 19 September 2024

[go] Đọc code prometheus xem vì sao delta tính sai kết quả

Prometheus là gì

The Prometheus monitoring system and time series /ˈsɪr.iːz/ database. Prometheus, a Cloud Native Computing Foundation project, is a systems and service monitoring system. It collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts when specified conditions are observed.

Prometheus là tên phần mềm metric monitoring tiêu chuẩn ngày nay. 10 năm trước là graphite. Stack phổ biến thời cloud: Prometheus lưu trữ time series, hiển thị biểu đồ dùng grafana, gửi cảnh báo alert dùng alertmanager.

Prometheus viết bằng Go, bắt nguồn từ SoundCloud, code xem tại https://github.com/prometheus/prometheus

Prometheus is an open-source systems monitoring and alerting toolkit originally built at SoundCloud. Since its inception in 2012, many companies and organizations have adopted Prometheus, and the project has a very active developer and user community. It is now a standalone open source project and maintained independently of any company. To emphasize this, and to clarify the project's governance structure, Prometheus joined the Cloud Native Computing Foundation in 2016 as the second hosted project, after Kubernetes.

Cài và chạy Prometheus

Cài đặt bằng việc tải file binary. Hay build từ source:

$ git clone https://github.com/prometheus/prometheus --branch v2.54.1
Cloning into 'prometheus'...
...
Receiving objects: 100% (118939/118939), 200.60 MiB | 7.23 MiB/s, done.
...

$ sudo apt install -y golang-1.21
...

Cài nodejs bản 20 để build frontend cho prometheus https://nodejs.org/en/download/package-manager

# installs nvm (Node Version Manager)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash

# download and install Node.js (you may need to restart the terminal)
nvm install 20

# verifies the right Node.js version is in the environment
node -v # should print `v20.17.0`

# verifies the right npm version is in the environment
npm -v # should print `10.8.2

Build prometheus:

$ cd prometheus
$ make build
cd web/ui && npm install

changed 1609 packages, and audited 1615 packages in 28s

...
>> building binaries
.../promu build --prefix /home/hvn/code/prometheus
 >   prometheus
go: downloading github.com/KimMachineGun/automemlimit v0.6.1
go: downloading github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30
...
>> building binaries
...bin/promu build --prefix /home/hvn/code/prometheus
 >   prometheus

Tạo file config https://prometheus.io/docs/introduction/first_steps/#configuring-prometheus:

$ cat <<EOF > prometheus.yaml
global:
  scrape_interval:     15s
  evaluation_interval: 15s

rule_files:
  # - "first.rules"
  # - "second.rules"

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ['localhost:9090']
EOF

Chạy:

$ ./prometheus --config.file=./prometheus.yaml
ts=2024-09-12T13:28:55.525Z caller=main.go:601 level=info msg="No time or size retention was set so using the default time retention" duration=15d
...
ts=2024-09-12T13:29:14.998Z caller=web.go:571 level=info component=web msg="Start listening for connections" address=0.0.0.0:9090
...

Xem metric trên giao diện

Mở trình duyệt địa chỉ http://127.0.0.1:9090

Gõ http sẽ có "auto complete" để chọn metric prometheus_http_requests_total

prom_query

Chọn label handler="/api/v1/query":

prometheus_http_requests_total{handler="/api/v1/query"}

Các metric này của chính chương trình Prometheus để tự monitor chính mình. Cụ thể, time series trên đếm số lượt truy cập tới đường dẫn /api/v1/query, mỗi lần người dùng query metric qua giao diện trên sẽ tăng giá trị thêm 1.

Instant vector selector

Instant vector selectors allow the selection of a set of time series and a single sample value for each at a given timestamp (point in time). In the simplest form, only a metric name is specified, which results in an instant vector containing elements for all time series that have this metric name.

Instant vector selector trả về các time series và 1 giá trị cho mỗi time series tại thời điểm hiện tại.

Ví dụ: prometheus_http_requests_total{handler="/api/v1/query"} trả về giá trị cho các label code khác nhau:

prometheus_http_requests_total{code="200", handler="/api/v1/query", instance="localhost:9090", job="prometheus"} 88
prometheus_http_requests_total{code="400", handler="/api/v1/query", instance="localhost:9090", job="prometheus"} 1

Range vector selector

Range vector literals work like instant vector literals, except that they select a range of samples back from the current instant. Syntactically, a time duration is appended in square brackets ([]) at the end of a vector selector to specify how far back in time values should be fetched for each resulting range vector element.

Range vector selector hoạt động như Instant vector selector, ngoại trừ việc nó trả về 1 dãy các giá trị trong khoảng thời gian lựa chọn tới thời điểm hiện tại. Ví dụ:

prometheus_http_requests_total{handler="/api/v1/query"}[1m] trả về:

prometheus_http_requests_total{code="200", handler="/api/v1/query", instance="localhost:9090", job="prometheus"}
90 @1726745269.174
92 @1726745284.174
105 @1726745299.174
114 @1726745314.174

prometheus_http_requests_total{code="400", handler="/api/v1/query", instance="localhost:9090", job="prometheus"}
1 @1726745269.174
1 @1726745284.174
1 @1726745299.174
3 @1726745314.174

Do trong file config cấu hình scrape_interval=15s, trong 1m (1 phút) sẽ có 4 giá trị kèm thời gian trả về.

Các function

Query function cung cấp sẵn các function để tính toán. Như tính

  • sum: tổng
  • abs: giá trị tuyệt đối
  • avg_over_time: giá trị trung bình
  • rate: giá trị tăng trung bình mỗi giây
  • delta: hiệu của giá trị đầu và cuối

Có thể thấy các function này hoạt động trên mỗi time series trong cùng 1 metrics, đa phần chỉ có ý nghĩa khi dùng với range selector (lấy trung bình của số lượt truy cập mỗi giây trong vòng 1 phút vừa rồi).

Viết rate(prometheus_http_requests_total{handler="/api/v1/query"}[1m]) trả về lần lượt giá trị cho từng series của cùng metric prometheus_http_requests_total:

{code="200", handler="/api/v1/query", instance="localhost:9090", job="prometheus"} 0.02222222222222222
{code="400", handler="/api/v1/query", instance="localhost:9090", job="prometheus"} 0

Để tập trung, ta sẽ chỉ query code="200".

delta() tính sai?

Dùng range vector selector: prometheus_http_requests_total{code="200", handler="/api/v1/query"}[1m]

Trả về:

prometheus_http_requests_total{code="200", handler="/api/v1/query", instance="localhost:9090", job="prometheus"}
120 @1726745659.174
122 @1726745674.174
134 @1726745689.174
149 @1726745704.174

prom_functions

delta(v range-vector) calculates the difference between the first and last value of each time series element in a range vector v, returning an instant vector with the given deltas and equivalent labels.

https://github.com/prometheus/prometheus/blob/v2.54.1/docs/querying/functions.md#delta

Vậy nếu tính nhẩm có delta có giá trị là 149 - 120 = 29, nhưng kết quả lại là 38.666666666666. Vậy delta tính sai?

Khoan đã, tài liệu còn viết:

The delta is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if the sample values are all integers.

extrapolated là gì?

extrapolate dịch ra tiếng Việt là "ngoại suy", ở đây kết quả được suy ra từ số liệu đã có. Tại sao phải suy?

vì lấy giá trị thời gian cuối trừ giá trị đầu 1726745704 - 1726745659 = 45 giây, mà range cần lấy là 1m = 60 giây, nên tính năng của delta sẽ suy ra (149-120)/45 * 60 = 38.666666666666.

Vậy delta không tính sai, delta chỉ tính đúng như tài liệu của nó mô tả.

rate() tính đúng?

rate(v range-vector) calculates the per-second average rate of increase of the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. Also, the calculation extrapolates to the ends of the time range, allowing for missed scrapes or imperfect alignment of scrape cycles with the range's time period.

https://github.com/prometheus/prometheus/blob/v2.54.1/docs/querying/functions.md#rate

rate bằng (149-120)/45 = 0.64444444444 như mong đợi trong trường hợp này, nhưng tài liệu có nhắc tới "extrapolates" trong trường hợp khác.

Đọc code Prometheus

$ find . -name '*.go' | xargs grep delta
...
./promql/functions.go:  "delta":              funcDelta,
./promql/functions.go:  "idelta":             funcIdelta,

thấy file promql/functions.go là nơi chứa code của các function.

func funcDelta(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
    return extrapolatedRate(vals, args, enh, false, false)
}

// === rate(node parser.ValueTypeMatrix) (Vector, Annotations) ===
func funcRate(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
    return extrapolatedRate(vals, args, enh, true, true)
}

cả 2 đều gọi tới extrapolatedRate Xem online https://github.com/prometheus/prometheus/blob/v2.54.1/promql/functions.go#L71-L173

// extrapolatedRate is a utility function for rate/increase/delta.
// It calculates the rate (allowing for counter resets if isCounter is true),
// extrapolates if the first/last sample is close to the boundary, and returns
// the result as either per-second (if isRate is true) or overall.
func extrapolatedRate(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper, isCounter, isRate bool) (Vector, annotations.Annotations) {
    ...
    case len(samples.Floats) > 1:
        numSamplesMinusOne = len(samples.Floats) - 1
        firstT = samples.Floats[0].T
        lastT = samples.Floats[numSamplesMinusOne].T
        resultFloat = samples.Floats[numSamplesMinusOne].F - samples.Floats[0].F

    ...
    // Duration between first/last samples and boundary of range.
    durationToStart := float64(firstT-rangeStart) / 1000
    durationToEnd := float64(rangeEnd-lastT) / 1000

    sampledInterval := float64(lastT-firstT) / 1000
    averageDurationBetweenSamples := sampledInterval / float64(numSamplesMinusOne)

    // If the first/last samples are close to the boundaries of the range,
    // extrapolate the result. This is as we expect that another sample
    // will exist given the spacing between samples we've seen thus far,
    // with an allowance for noise.
    extrapolationThreshold := averageDurationBetweenSamples * 1.1
    extrapolateToInterval := sampledInterval

    ...
    extrapolateToInterval += durationToStart

    if durationToEnd >= extrapolationThreshold {
        durationToEnd = averageDurationBetweenSamples / 2
    }
    extrapolateToInterval += durationToEnd

    factor := extrapolateToInterval / sampledInterval
    if isRate {
        factor /= ms.Range.Seconds()
    }
    if resultHistogram == nil {
        resultFloat *= factor
    } else {
        resultHistogram.Mul(factor)
    }

    return append(enh.Out, Sample{F: resultFloat, H: resultHistogram}), annos
}

funcDelta có isRate=false isCounter=false, kết quả là

sampledInterval := float64(lastT-firstT) / 1000
factor := extrapolateToInterval / sampledInterval
resultFloat = samples.Floats[numSamplesMinusOne].F - samples.Floats[0].F
resultFloat * factor

hay

(giá trị cuối - giá trị đầu) * extrapolateToInterval / (thời gian sample cuối - thời gian sample đầu)
(149-120) * 60 /45 = 38.666666666666.

funcRate có isRate=true isCounter=true, kết quả là

sampledInterval := float64(lastT-firstT) / 1000
factor := extrapolateToInterval / sampledInterval
if isRate {
    factor /= ms.Range.Seconds()
}

resultFloat = samples.Floats[numSamplesMinusOne].F - samples.Floats[0].F
if !isCounter {
    break
}
// Handle counter resets:
prevValue := samples.Floats[0].F
resultFloat = samples.Floats[numSamplesMinusOne].F - samples.Floats[0].F
for _, currPoint := range samples.Floats[1:] {
    if currPoint.F < prevValue {
        resultFloat += prevValue
    }
    prevValue = currPoint.F
}
resultFloat * factor

hay

(giá trị sample cuối - giá trị sample đầu) * extrapolateToInterval / (thời gian sample cuối - thời gian sample đầu) / interval của range vector
(149-120) * 60 / 45 / 60 = 0.6444444444444444

đọc code trên thấy extrapolateToInterval thường cũng bằng với interval của range. Giá trị của rate có thể được extrapolated nếu series bị reset (Ví dụ: đang đếm tới 123 thì service được monitor bị restart đếm lại từ 0).

Kết luận

Prometheus function không tính sai, nó chỉ tính đúng như trong tài liệu mô tả. Hãy đọc tài liệu. Hoặc đọc code.

Hết.

HVN at http://pymi.vn and https://www.familug.org.

Ủng hộ đồng bào bị ảnh hưởng bởi cơn bão số 3.

Báo Tuổi Trẻ, Ngân hàng Công thương chi nhánh 3, TP.HCM. Số tài khoản: 113000006100 (Việt Nam đồng). Nội dung: Ủng hộ đồng bào bị ảnh hưởng bởi cơn bão số 3.

yagi

Tuesday, 10 September 2024

Đọc code bash xem vì sao echo * không hiện tên file ẩn

glob là gì

$ whatis glob
glob (7)             - globbing pathnames

man 7 glob có viết:

NAME
       glob - globbing pathnames

DESCRIPTION
       Long ago, in UNIX V6, there was a program /etc/glob that would expand wildcard patterns.  Soon afterward this became a shell built-in.

glob là khái niệm để "match" (chọn) filename sử dụng "pattern", gần giống với Regular Expression. Ví dụ trên shell gõ ls *.py sẽ hiện tên các file có đuôi .py.

Điểm khác biệt so với Regular Expression: glob chỉ dùng để match filename, và các ký tự đặc biệt trong glob có ý nghĩa khác so với Regular Expression (ví dụ * có nghĩa khác).

Wildcard pattern là gì

Một string là 1 wildcard pattern nếu chứa ? * hoặc [, glob là việc biến wildcard pattern thành list các filename "match" pattern.

   A '?' (not between brackets) matches any single character.
   A '*' (not between brackets) matches any string, including the empty string.

Ví dụ

$ touch error.c error.h
$ echo er??r.c
error.c
$ echo error.*
error.c  error.h

Một điểm đáng chú ý là * không match các filename bắt đầu bằng dấu . - hay còn gọi là file ẩn (hidden file) trên các hệ điều hành UNIX/Linux. Muốn chọn cả các file bắt đầu bằng ., phải dùng .*.

$ touch error.c error.h
$ touch .flag
$ echo *
error.c error.h
$ echo .*
.flag

Đọc code bash xem tại sao glob echo * không hiện filename bắt đầu bằng dấu .

Tải code bash 5.1 về

$ wget https://ftp.gnu.org/gnu/bash/bash-5.1.16.tar.gz
--2024-09-10 21:59:27--  https://ftp.gnu.org/gnu/bash/bash-5.1.16.tar.gz
Resolving ftp.gnu.org (ftp.gnu.org)... 209.51.188.20, 2001:470:142:3::b
Connecting to ftp.gnu.org (ftp.gnu.org)|209.51.188.20|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 10522932 (10M) [application/x-gzip]
Saving to: ‘bash-5.1.16.tar.gz’

bash-5.1.16.tar.gz           100%[============================================>]  10,04M  3,52MB/s    in 2,8s

2024-09-10 21:59:31 (3,52 MB/s) - ‘bash-5.1.16.tar.gz’ saved [10522932/10522932]

$ tar xf bash-5.1.16.tar.gz
$ cd bash-5.1.16

Tìm từ khóa glob

$ find . -name '*.c' | xargs grep -l glob
...
./lib/glob/gmisc.c
./lib/glob/smatch.c
./lib/glob/xmbsrtowcs.c
./lib/glob/strmatch.c
./lib/glob/glob_loop.c
./lib/glob/sm_loop.c
./lib/glob/glob.c
...

Thấy có riêng 1 thư mục tên lib/glob.

$ head glob.c
/* glob.c -- file-name wildcard pattern matching for Bash.
...
/* To whomever it may concern: I have never seen the code which most
   Unix programs use to perform this function.  I wrote this from scratch
   based on specifications for the pattern matching.  --RMS.  */

Viết bởi RMS - Richard Stallman tác giả của GNU.

Trong file này chỉ có 10 function:

static int
extglob_skipname (pat, dname, flags)
--
static int
skipname (pat, dname, flags)
--
static int
wskipname (pat, dname, flags)
--
static int
wextglob_skipname (pat, dname, flags)
--
static int
mbskipname (pat, dname, flags)
--
static void
wdequote_pathname (pathname)
--
static void
dequote_pathname (pathname)
--
static int
glob_testdir (dir, flags)
--
static struct globval *
finddirs (pat, sdir, flags, ep, np)
--
static char **
glob_dir_to_array (dir, array, flags)

nhìn tên đoán skipname có vẻ là thứ cần tìm:

/* Return 1 if DNAME should be skipped according to PAT.  Mostly concerned
   with matching leading `.'. */
static int
skipname (pat, dname, flags)
     char *pat;
     char *dname;
     int flags;
{
#if EXTENDED_GLOB
  if (extglob_pattern_p (pat))      /* XXX */
    return (extglob_skipname (pat, dname, flags));
#endif

  if (glob_always_skip_dot_and_dotdot && DOT_OR_DOTDOT (dname))
    return 1;

  /* If a leading dot need not be explicitly matched, and the pattern
     doesn't start with a `.', don't match `.' or `..' */
  if (noglob_dot_filenames == 0 && pat[0] != '.' &&
    (pat[0] != '\\' || pat[1] != '.') &&
    DOT_OR_DOTDOT (dname))
    return 1;

  /* If a dot must be explicitly matched, check to see if they do. */
  else if (noglob_dot_filenames && dname[0] == '.' && pat[0] != '.' &&
    (pat[0] != '\\' || pat[1] != '.'))
    return 1;

  return 0;
}

Comment rất rõ ràng:

Return 1 if DNAME should be skipped according to PAT. Mostly concerned with matching leading .

Xem trong các điều kiện return 1 trong function trên, thấy

  else if (noglob_dot_filenames && dname[0] == '.' && pat[0] != '.' &&
    (pat[0] != '\\' || pat[1] != '.'))
    return 1;

nếu noglob_dot_filenames và filename tại index 0 có giá trị là . dname[0] == '.' và kí tự đầu tiên trong pattern không phải . pat[0] != '.' thì return 1 - tức bỏ qua file này.

noglob_dot_filenames là 1 global var:

/* Global variable which controls whether or not * matches .*.
   Non-zero means don't match .*.  */
int noglob_dot_filenames = 1;

Chạy debugger lldb xem code chạy

Thay vì các bài trước sửa code, thêm print, build lại, bài này dùng C debugger LLDB để debug và in ra.

Clang là C compiler hiện đại, có debugger tương ứng là LLDB, combo hiện đại này xuất hiện trên MacOS và các hệ điều hành BSD khác. Combo khác hay dùng là gcc và gdb.

$ sudo apt install -y clang lldb
...

$ CC=clang ./configure
$ make -j `nproc`
...
make -j `nproc`  28,47s user 3,28s system 364% cpu 8,716 total
$ file ./bash
./bash: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=cbc1d1b6f48f5881c417e988c2253f29d1a5900d, for GNU/Linux 3.2.0, with debug_info, not stripped

Mất 8s trên máy AMD Ryzen 3 4300U.

Các lệnh cơ bản của LLDB

  • b skipname : tạo breakpoint tại function skipname
  • process launch -- -c 'echo *' chạy chương trình với argument -c 'echo *'
  • bt - in ra backtrace, hiển thị call-stack - tức function nào gọi function nào.
  • n chạy đến dòng code tiếp theo
  • c chạy đến breakpoint tiếp theo
  • frame var in ra các variable trong frame hiện tại
  • finish kết thúc function và in ra return value.

gõ lệnh xong gõ enter để chạy.

dùng lldb chạy chương trình bash với argument -c 'echo *'

$ mkdir test
$ cd test
$ touch main.py lib.py .flag
$ lldb ../bash
Traceback (most recent call last):
(lldb) target create "../bash"
Current executable set to '/home/me/bash-5.1.16/bash' (x86_64).

(lldb) b skipname
Breakpoint 1: where = bash`skipname + 22 at glob.c:268:7, address = 0x00000000000b0fe6
(lldb) launch process -- -c 'echo *'
error: 'launch' is not a valid command.
(lldb) process launch -- -c 'echo *'
Process 17373 launched: '/home/me/bash-5.1.16/bash' (x86_64)
Process 17373 stopped
* thread #1, name = 'bash', stop reason = breakpoint 1.1
    frame #0: 0x0000555555604fe6 bash`skipname(pat="*", dname=".", flags=<unavailable>) at glob.c:268:7
   265       int flags;
   266  {
   267  #if EXTENDED_GLOB
-> 268    if (extglob_pattern_p (pat))      /* XXX */
   269      return (extglob_skipname (pat, dname, flags));
   270  #endif
   271
(lldb) bt
* thread #1, name = 'bash', stop reason = breakpoint 1.1
  * frame #0: 0x0000555555604fe6 bash`skipname(pat="*", dname=".", flags=<unavailable>) at glob.c:268:7
    frame #1: 0x0000555555604d68 bash`mbskipname(pat=<unavailable>, dname=<unavailable>, flags=<unavailable>) at glob.c:386:13 [artificial]
    frame #2: 0x00005555556040cf bash`glob_vector(pat="*", dir=".", flags=256) at glob.c:812:26
    frame #3: 0x00005555556053e0 bash`glob_filename(pathname=<unavailable>, flags=0) at glob.c:1492:22
    frame #4: 0x00005555555d09fc bash`shell_glob_filename(pathname=<unavailable>, qflags=8) at pathexp.c:470:13
    frame #5: 0x00005555555c2b46 bash`expand_word_list_internal [inlined] glob_expand_word_list(tlist=0x00005555556acc90, eflags=<unavailable>) at subst.c:11409:17
    frame #6: 0x00005555555c2aca bash`expand_word_list_internal(list=<unavailable>, eflags=31) at subst.c:12022:13
    frame #7: 0x00005555555c2135 bash`expand_words(list=<unavailable>) at subst.c:11357:11 [artificial]
    frame #8: 0x000055555559aa00 bash`execute_simple_command(simple_command=0x00005555556ac850, pipe_in=-1, pipe_out=-1, async=0, fds_to_close=0x00005555556ac910) at execute_cmd.c:4381:15
    frame #9: 0x0000555555599638 bash`execute_command_internal(command=0x00005555556ac810, asynchronous=0, pipe_in=-1, pipe_out=-1, fds_to_close=0x00005555556ac910) at execute_cmd.c:846:4
    frame #10: 0x00005555555f0f87 bash`parse_and_execute(string="echo *", from_file="-c", flags=20) at evalstring.c:489:17
    frame #11: 0x0000555555585f60 bash`run_one_command(command="echo *") at shell.c:1440:12
    frame #12: 0x00005555555854f3 bash`main(argc=3, argv=0x00007fffffffe6c8, env=<unavailable>) at shell.c:741:7
    frame #13: 0x00007ffff7c29d90 libc.so.6`__libc_start_call_main(main=(bash`main at shell.c:368), argc=3, argv=0x00007fffffffe6c8) at libc_start_call_main.h:58:16
    frame #14: 0x00007ffff7c29e40 libc.so.6`__libc_start_main_impl(main=(bash`main at shell.c:368), argc=3, argv=0x00007fffffffe6c8, init=0x00007ffff7ffd040, fini=<unavailable>, rtld_fini=<unavailable>, stack_end=0x00007fffffffe6b8) at libc-start.c:392:3
    frame #15: 0x0000555555583de5 bash`_start + 37
(lldb) n
(lldb) frame var
(char *) pat = <variable not available>

(char *) dname = 0x00005555556af053 "."
(int) flags = <no location, value may have been optimized out>
(lldb) c
Process 17373 resuming
Process 17373 stopped
* thread #1, name = 'bash', stop reason = breakpoint 1.1
    frame #0: 0x0000555555604fe6 bash`skipname(pat="*", dname=".flag", flags=<unavailable>) at glob.c:268:7
   265       int flags;
   266  {
   267  #if EXTENDED_GLOB
-> 268    if (extglob_pattern_p (pat))      /* XXX */
   269      return (extglob_skipname (pat, dname, flags));
   270  #endif
   271

(lldb) frame var
(char *) pat = <variable not available>

(char *) dname = 0x00005555556af06b ".flag"
(int) flags = <no location, value may have been optimized out>

Tới đây ta có filename đang được xét là .flag,và pat="*"

    frame #0: 0x0000555555604fe6 bash`skipname(pat="*", dname=".flag", flags=<unavailable>) at glob.c:268:7

bấm n cho tới khi thấy

(lldb)  n
Process 17373 stopped
* thread #1, name = 'bash', stop reason = step over
    frame #0: 0x0000555555605180 bash`skipname(pat=<unavailable>, dname=".flag", flags=<unavailable>) at glob.c:283:45
   280      return 1;
   281
   282    /* If a dot must be explicitly matched, check to see if they do. */
-> 283    else if (noglob_dot_filenames && dname[0] == '.' && pat[0] != '.' &&
   284      (pat[0] != '\\' || pat[1] != '.'))
   285      return 1;
   286
(lldb)
Process 17373 stopped
* thread #1, name = 'bash', stop reason = step over
    frame #0: 0x00005555556051e3 bash`skipname(pat=<unavailable>, dname=".flag", flags=<unavailable>) at glob.c:288:1
   285      return 1;
   286
   287    return 0;
-> 288  }
   289
(lldb) finish
* thread #1, name = 'bash', stop reason = step out
Stepped out past: frame #1: 0x0000555555604d68 bash`mbskipname(pat=<no value available>, dname=<no value available>, flags=<no summary available>) at glob.c:386:13 [artificial]

Return value: (int) $13 = 1

thấy sau khi finish function trả về 1 tức skip file .flag.

Làm tương tự, thấy trả về 0 với lib.py:

(lldb) c
Process 17373 resuming
Process 17373 stopped
* thread #1, name = 'bash', stop reason = breakpoint 8.1
    frame #0: 0x0000555555604fe6 bash`skipname(pat="*", dname="lib.py", flags=<unavailable>) at glob.c:268:7
   265       int flags;
   266  {
   267  #if EXTENDED_GLOB
-> 268    if (extglob_pattern_p (pat))      /* XXX */
   269      return (extglob_skipname (pat, dname, flags));
   270  #endif
   271
(lldb) finish
Process 17373 stopped
* thread #1, name = 'bash', stop reason = step out
Stepped out past: frame #1: 0x0000555555604d68 bash`mbskipname(pat=<no value available>, dname=<no value available>, flags=<no summary available>) at glob.c:386:13 [artificial]

Return value: (int) $14 = 0

Tham khảo

man 7 glob

Kết luận

glob không match file ẩn có tên bắt đầu bằng .. Đọc code C khó quá thì chạy debugger LLDB.

Written using

$ bash --version
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)

Hết.

HVN at http://pymi.vn and https://www.familug.org.

Ủng hộ đồng bào bị ảnh hưởng bởi cơn bão số 3.

Báo Tuổi Trẻ, Ngân hàng Công thương chi nhánh 3, TP.HCM. Số tài khoản: 113000006100 (Việt Nam đồng). Nội dung: Ủng hộ đồng bào bị ảnh hưởng bởi cơn bão số 3.

yagi

Monday, 2 September 2024

bash chậm tới mức nào?

trên những môi trường không có ngôn ngữ nào khác, bash vẫn là lựa chọn duy nhất:

  • docker entrypoint (script chạy 1 chương trình phức tạp trong docker)

bash không dùng để viết chương trình cần hiệu năng cao, nhưng liệu bash chậm tới mức nào?

Tính tổng từ 1 đến 1 triệu

#1
x=0
for i in {1..1000000} ; do
     x=$((x+i))
done
echo "$x"

#2
x=0
for i in $(seq 1 1000000) ; do
     x=$((x+i))
done
echo "$x"

#3
x=0
for ((i=1;i<=1000000;i++)); do
     x=$((x+i))
done
echo "$x"

#4
x=0
i=1
while [ $i -le 1000000 ]; do
    x=$((x+i))
    i=$((i+1))
done
echo "$x"

Trong 4 cách trên,

  • cách 1 với {1..1000000} tạo 1 triệu phần tử trong RAM (như Python dùng list(range(1,1_000_001))), dùng hết 284MB RAM.
  • cách 2 với seq 1 1000000 chỉ dùng 3.7MB RAM, nhưng phụ thuộc vào chương trình seq bên ngoài
  • cách 3 và 4 tương tự nhau nhưng cách 4 chạy trên tất cả cách loại (POSIX) shell.

Dùng shellcheck để biết cách 3 không chạy trên sh:

$ sudo apt install -y shellcheck
$ shellcheck --shell sh slow.sh

In slow.sh line 2:
for i in {1..1000000} ; do
         ^----------^ SC3009 (warning): In POSIX sh, brace expansion is undefined.


In slow.sh line 16:
for ((i=1;i<=1000000;i++)); do
^-^ SC3005 (warning): In POSIX sh, arithmetic for loops are undefined.
                      ^-- SC3018 (warning): In POSIX sh, ++ is undefined.

For more information:
  https://www.shellcheck.net/wiki/SC3005 -- In POSIX sh, arithmetic for loops...
  https://www.shellcheck.net/wiki/SC3009 -- In POSIX sh, brace expansion is u...
  https://www.shellcheck.net/wiki/SC3018 -- In POSIX sh, ++ is undefined.

Chạy cách 1 với {1..1000000}:

$ /usr/bin/time -v bash slow.sh
500000500000
    Command being timed: "bash slow.sh"
    User time (seconds): 2.14
    System time (seconds): 0.09
    Percent of CPU this job got: 99%
    Elapsed (wall clock) time (h:mm:ss or m:ss): 0:02.24
...
    Maximum resident set size (kbytes): 284852
...
    Minor (reclaiming a frame) page faults: 72418
    Voluntary context switches: 1
    Involuntary context switches: 10
...

Chạy cách 4 với while:

$ /usr/bin/time -v bash slow.sh
500000500000    Command being timed: "bash slow.sh"
    User time (seconds): 5.05
    System time (seconds): 0.00
    Percent of CPU this job got: 99%
    Elapsed (wall clock) time (h:mm:ss or m:ss): 0:05.05
...
    Maximum resident set size (kbytes): 3712
...
    Minor (reclaiming a frame) page faults: 155
    Voluntary context switches: 1
    Involuntary context switches: 27
...

Mất 5 giây để tính, trong khi với Python - 1 trong những ngôn ngữ lập trình chậm nhất, chỉ mất 0.08 giây tính cả thời gian bật Python, dùng 9MB RAM. Với Python 1 giây có thể tính tổng tới 15 triệu, tức bash chậm hơn Python 15*5 = 75 lần.

$ /usr/bin/time -v python3 sum.py
500000500000
    Command being timed: "python3 sum.py"
    User time (seconds): 0.08
    System time (seconds): 0.00
    Percent of CPU this job got: 100%
    Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.08
...
    Maximum resident set size (kbytes): 9088
...
    Minor (reclaiming a frame) page faults: 943
    Voluntary context switches: 1
...

1 điều đáng chú ý là dùng dash - 1 POSIX shell có sẵn trên Ubuntu chạy nhanh hơn và tốn ít RAM hơn bash:

$ /usr/bin/time -v dash slow.sh
500000500000    Command being timed: "dash slow.sh"
    User time (seconds): 1.76
    System time (seconds): 0.00
    Percent of CPU this job got: 99%
    Elapsed (wall clock) time (h:mm:ss or m:ss): 0:01.76
    Average shared text size (kbytes): 0
    Average unshared data size (kbytes): 0
    Average stack size (kbytes): 0
    Average total size (kbytes): 0
    Maximum resident set size (kbytes): 1536
    Average resident set size (kbytes): 0
    Major (requiring I/O) page faults: 0
    Minor (reclaiming a frame) page faults: 78
    Voluntary context switches: 1
    Involuntary context switches: 45
    Swaps: 0
    File system inputs: 0
    File system outputs: 0
    Socket messages sent: 0
    Socket messages received: 0
    Signals delivered: 0
    Page size (bytes): 4096
    Exit status: 0

Chậm thì sao?

Khi dùng để chạy các chương trình trong container, nếu không hoàn thành quá trình trong thời gian giới hạn, container sẽ bị restart để chạy lại từ đầu, và khi chạy lại thì vẫn chậm, vẫn không kịp, lại restart, ... tạo thành 1 vòng lặp vô hạn. Mặc dù ví dụ trên bash chạy 5 giây, nhưng khi dùng trong các container với 250m CPU (1/4 CPU), bash sẽ mất ~20 giây để chạy, trên thực tế, mất gần 50 giây:

$ systemd-run --scope --uid=1000 -p CPUQuota=25% /usr/bin/time -v bash slow.sh
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===
Authentication is required to manage system services or other units.
Authenticating as: username
Password:
==== AUTHENTICATION COMPLETE ===
Running scope as unit: run-u139.scope
500000500000    Command being timed: "bash slow.sh"
    User time (seconds): 12.32
    System time (seconds): 0.00
    Percent of CPU this job got: 25%
    Elapsed (wall clock) time (h:mm:ss or m:ss): 0:49.25
...
    Maximum resident set size (kbytes): 3712
    Average resident set size (kbytes): 0
    Major (requiring I/O) page faults: 0
    Minor (reclaiming a frame) page faults: 152
    Voluntary context switches: 1
    Involuntary context switches: 623

Kết luận

bash chậm.

Written using

$ bash --version
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)

Hết.

HVN at http://pymi.vn and https://www.familug.org.

Ủng hộ tác giả 🍺