New FAMILUG

The PyMiers

Friday, 29 November 2024

Người mới (newbie) có nên viết bài?

Có.

Người mới (trong một vấn đề X) có những ưu thế nhất định:

  • góc nhìn mới: người có kinh nghiệm thường đã quen với vấn đề tồn tại, biết cách né tránh nó, dần khiến họ cảm thấy nó là bình thường, 1 chút bất tiện nhỏ, và không dẫn đến thay đổi tốt hơn. Một nhân viên mới 2 3 tháng đầu là thời gian quý báu nhất mà nhà tuyển dụng cần tận dụng để tìm ra các vấn đề của hệ thống/ quy trình hiện tại.
  • sai lầm là bình thường: người mới có thể sẽ viết sai, nhưng phần mềm nào mà không có bug, kể cả viết bởi các thiên tài? vậy cớ gì một bài viết của 1 người bình thường không được quyền có bug? sai lầm là một phần tất yếu của trưởng thành.
  • sự mới mẻ: người ta luôn thích cái mới, người mới viết bài sẽ viết bài có tính tích cực, vui vẻ, khiến xã hội ngày càng tươi đẹp, nở hoa.
  • viết giúp cho suy nghĩ: khi viết 100 dòng, người viết cần ít nhất 1 giờ để viết, và khi viết, cần nhiều thời gian để tìm hiểu để viết cho gần đúng nhất, khiến hiểu về vấn đề X rõ hơn. Đọc chỉ mất 10 phút, và không cần nghĩ.

Kết luận

Có, hãy viết để phát triển văn hóa sản xuất của tầng lớp lao động, thay cho văn hóa hưởng thụ của tầng lớp quý tộc ngày một lên ngôi.

Hết.

Tham khảo

No, thanks.

Wednesday, 2 October 2024

Prometheus căn bản

Căn bản không nghĩa là đơn giản hay dễ. Điều này càng đúng khi nói về time series database (TSDB) như Prometheus. Time series là 1 nhánh của toán học, mà chỉ có 1 thứ dễ khi nói về toán, đó là dễ sai. Thời gian là 1 khái niệm không được định nghĩa rõ ràng, chỉ có trong trí tưởng tượng của con người https://en.wikipedia.org/wiki/Time.

Chú ý: vì vậy bài viết này, giống mọi bài viết khác, có thể sai.

Bài viết giúp trả lời các câu hỏi:

  • Time series là gì
  • Metric và label là gì
  • Các kiểu metric
  • Các kiểu dữ liệu trong PromQL
  • Instant vector khác gì range vector
  • Instant vector selector khác gì range vector selector
  • Instant query khác gì range query
  • Operator khác gì function [bài sau]

Cài và chạy Prometheus

Xem bài trước.

Các khái niệm trong Prometheus

Data model - Timeseries

Data model

Prometheus fundamentally stores all data as time series: streams of timestamped values belonging to the same metric and the same set of labeled dimensions.

Prometheus chứa tất cả dữ liệu như các time series (chuỗi thời gian). Time series: một chuỗi giá trị gắn liền thời gian thuộc về cùng 1 metric và cùng label.

Ví dụ:

2 @1726148884.177
19 @1726148899.177

value có kiểu float 64 bits, timestamp được tính bằng giây từ mốc Unix Epoch 1/1/1970, với phần milli giây sau dấu .. Kí hiệu @ dùng khi hiển thị ở giao diện, để dễ phân biệt đó là phần timestamp.

prom_query

Sample

Doc

Một cặp (value, timestamp) gọi là 1 sample.

Metric names

Metric name là tên dùng để đại diện cho thứ được đo

Ví dụ: prometheus_http_requests_total

Metric label

Kèm với metric name, có thể dùng các cặp key-value gọi là label, mỗi label tạo thành 1 chiều (dimension) để chứa các thuộc tính khác của 1 metric.

Ví dụ:

prometheus_http_requests_total{code="200"} 10
prometheus_http_requests_total{code="500"} 2

Cú pháp:

<metric name>{<label name>=<label value>, <label name2>=<label value2>, ...}

Metric name kết hợp với đầy đủ label xác định 1 time series cụ thể. Metric name thực chất cũng là 1 label (__name__), vậy nên có thể nói dùng labelset (tập các label) để xác định 1 time series cụ thể.

Các kiểu metric

Doc

Có 4 kiểu metric:

  • Counter: counter để đếm, mỗi lần chỉ có thể giữ nguyên hay tăng lên 1 đơn vị, hoặc reset về 0. Ví dụ: đếm (tổng) số lượt truy cập website, (tổng) số lỗi xảy ra...
  • Gauge: /ɡeɪdʒ/ để đo các giá trị có thể tăng giảm tùy ý, ví dụ %CPU, %RAM.
  • Histogram: đếm các giá trị trong các khoảng chia trước (gọi là bucket). Ví dụ chia latency truy cập vào website thành các mốc 0.1s 0.2s 0.3s 0.45s, histogram metric đếm số giá trị trong các khoảng le (lower or equal) {le="0.1"} tức từ 0 đến 0.1 [0,0.1], {le=",0.2"} tức [0,0.2], {le="0.3"} tức [0,0.3], {le="0.45"} [0,0.45] và {le="+inf} [0,+inf]. Khi 1 request có latency là 0.33, các bucket {le="0.45"} và {le="+inf"} sẽ có giá trị tăng thêm 1.
  • Summary: do tác giả bài viết chưa dùng bao giờ nên bạn đọc vui lòng xem tài liệu.

Xem thêm giải thích + hình minh họa tại https://prometheus.io/docs/tutorials/understanding_metric_types/

Các kiểu dữ liệu trong PromQL

Prometheus Server > Querying > Basic https://prometheus.io/docs/prometheus/latest/querying/basics/ viết

Prometheus provides a functional query language called PromQL (Prometheus Query Language) that lets the user select and aggregate time series data in real time.

Prometheus sử dụng 1 ngôn ngữ query tên là PromQL (Prometheus Query Language). PromQL có 4 kiểu dữ liệu (data type):

  • Instant vector - a set of time series containing a single sample for each time series, all sharing the same timestamp
  • Range vector - a set of time series containing a range of data points over time for each time series
  • Scalar - a simple numeric floating point value
  • String - a simple string value; currently unused

Instant vector

Instant vector là 1 tập các time series, mỗi time series chứa 1 sample, tất cả đều có cùng timestamp. Ví dụ prometheus_http_requests_total trả về 1 instant vector, chứa 2 timeseries:

prometheus_http_requests_total{code="200"} 10
prometheus_http_requests_total{code="500"} 2

Range vector

Range vector (còn gọi là Matrix): là 1 tập các time series, mỗi time series chứa một khoảng các sample.

Code https://github.com/prometheus/prometheus/blob/v2.53.0/promql/value.go#L31-L34

func (Matrix) Type() parser.ValueType { return parser.ValueTypeMatrix }
func (Vector) Type() parser.ValueType { return parser.ValueTypeVector }
func (Scalar) Type() parser.ValueType { return parser.ValueTypeScalar }
func (String) Type() parser.ValueType { return parser.ValueTypeString }
// Series is a stream of data points belonging to a metric.
type Series struct {
    Metric     labels.Labels `json:"metric"`
    Floats     []FPoint      `json:"values,omitempty"`
    Histograms []HPoint      `json:"histograms,omitempty"`
}
// Vector is basically only an alias for []Sample, but the contract is that
// in a Vector, all Samples have the same timestamp.
type Vector []Sample
// Matrix is a slice of Series that implements sort.Interface and
// has a String method.
type Matrix []Series

Khi nói tới Instant vector và Range vector là nói về kiểu dữ liệu (data type).

Time series selectors

Doc GitHub Web

Cú pháp để chọn các time series, gồm 2 loại là instant vector selector và range vector selector:

instant vector selectors

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).

Instant vector selector chọn 1 tập các time series, mỗi time series 1 sample tại timestamp được chọn. Ví dụ 4 instant vector selector:

prometheus_http_requests_total
prometheus_http_requests_total{code="200"}
prometheus_http_requests_total{code!="200"}
prometheus_http_requests_total{code="200", handler="/api/v1/query"}

Có thể dùng regex để chọn label value

prometheus_http_requests_total{code=~".+00", handler=~"/api/v1/query.*"}

Kết quả:

prometheus_http_requests_total{code="200", handler="/api/v1/query", instance="localhost:9090", job="prometheus"} 7
prometheus_http_requests_total{code="200", handler="/api/v1/query_exemplars", instance="localhost:9090", job="prometheus"} 0
prometheus_http_requests_total{code="200", handler="/api/v1/query_range", instance="localhost:9090", job="prometheus"} 0
prometheus_http_requests_total{code="400", handler="/api/v1/query", instance="localhost:9090", job="prometheus"} 4

Tên metric là 1 label đặc biệt __name__:

{__name__=~"prometheus_http_requests.*", code=~".+00", handler=~"/api/v1/query.*"}

cũng cho kết quả như bên trên.

range vector selectors

Range vector literals work like instant vector literals, except that they select a range of samples back from the current instant.

Range vector selector hoạt động giống instant vector selector, ngoại trừ việc nó chọn 1 khoảng các sample từ thời điểm hiện tại về trước. Khoảng thời gian được viết sau metric name và label, đặt trong dấu [], có đơn vị s h m d w y. 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

Có thể đổi time offset 1 tuần trước

prometheus_http_requests_total{handler="/api/v1/query"} offset 1w

Có thể chọn thời gian tính toán kết quả

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

Xem chi tiết tại https://prometheus.io/docs/prometheus/2.53/querying/basics/#offset-modifier

Instant query và range query

Instant query và range query là 2 API của Prometheus.

Instant query

Doc Code

The following endpoint evaluates an instant query at a single point in time:

GET /api/v1/query
POST /api/v1/query

API này trả về kết quả của PromQL query tại 1 thời điểm (mặc định là thời điểm hiện tại), xem ở dạng bảng (table):

instant query

The data section of the query result has the following format:

{ "resultType": "matrix" | "vector" | "scalar" | "string", "result": }

Query dùng instant vector selector up trả về vector

$ curl 'http://localhost:9090/api/v1/query?query=up&time=1728562908.678'
{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"up","instance":"localhost:9090","job":"prometheus"},"value":[1728562908.678,"1"]}]}}

Query dùng range vector selector up[1m] trả về matrix

$ curl 'http://localhost:9090/api/v1/query?query=up%5B1m%5D&time=1728563140.405'
{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"__name__":"up","instance":"localhost:9090","job":"prometheus"},"values":[[1728563087.296,"1"],[1728563102.296,"1"],[1728563117.294,"1"],[1728563132.297,"1"]]}]}}

Instant query thường được Prometheus dùng khi tính toán các rule để gửi alert.

Range query

Doc Code

The following endpoint evaluates an expression query over a range of time:

GET /api/v1/query_range
POST /api/v1/query_range

API này sẽ trả về kết quả của PromQL query tại 1 khoảng thời gian. Nó chỉ nhận 2 loại kiểu dữ liệu là instant vector và scalar. Thử query up[1m] sẽ nhận được lỗi:

$ curl 'http://localhost:9090/api/v1/query_range?query=up%5B1m%5D&start=1728558788.318&end=1728562388.318&step=14'
{"status":"error","errorType":"bad_data","error":"invalid parameter \"query\": invalid expression type \"range vector\" for range query, must be Scalar or instant Vector"}

Range query giống như thực hiện instant query qua nhiều thời điểm khác nhau rồi gộp kết quả lại thành 1 matrix. Kết quả là mỗi time series sẽ có nhiều sample và có thể dùng vẽ đồ thị.

range query

Range query chủ yếu dùng để vẽ đồ thị (graph), phần mềm vẽ đồ thị như Grafana sẽ gọi qua API này.

Xem hình vẽ từ blog PromLab mô tả cách range query thực hiện:

range query explain

Kết luận

Prometheus cơ bản thật không hề đơn giản.

Hết.

Tham khảo

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ả 🍺

Wednesday, 28 August 2024

Đọc code xem NGINX auto dùng bao nhiêu worker_processes trong container

NGINX là HTTP server phổ biến nhất trên thế giới. NGINX có kiến trúc "master" - "worker", với master process làm nhiệm vụ quản lý, và các worker process sẽ nhận các HTTP request và xử lý.

NGINX worker_processes là gì

Số lượng worker process được set bằng config directive: worker_processes mặc định là 1, và có thể "tự động" với từ khóa auto.

Syntax:     worker_processes number | auto;
Default:

worker_processes 1;

Context:    main

Defines the number of worker processes.

The optimal value depends on many factors including (but not limited to) the number of CPU cores, the number of hard disk drives that store data, and load pattern. When one is in doubt, setting it to the number of available CPU cores would be a good start (the value “auto” will try to autodetect it).

    The auto parameter is supported starting from versions 1.3.8 and 1.2.5.

https://nginx.org/en/docs/ngx_core_module.html#worker_processes

khi set auto, NGINX sẽ try to autodetect - cố tìm số "available CPU cores", dễ dàng xem số CPU trên máy với câu lệnh

$ nproc
4

hay file /proc/cpuinfo

$ grep processor /proc/cpuinfo
processor   : 0
processor   : 1
processor   : 2
processor   : 3

auto là bao nhiêu?

"auto" là bao nhiêu? lấy giá trị từ đâu? có đọc từ /proc ra không? thử đọc code C xem viết gì:

$ git clone --depth 1 https://github.com/nginx/nginx --branch release-1.27.1
Cloning into 'nginx'...
remote: Enumerating objects: 555, done.
...
Note: switching to 'e06bdbd4a20912c5223d7c6c6e2b3f0d6086c928'.
...

$ cd nginx
$ grep -Rn 'define NGINX_VERSION' src/core/nginx.h
13:#define NGINX_VERSION      "1.27.1"

Tìm từ khóa auto:

$ grep -Rin '"auto"'
...
src/core/nginx.c:1425:    if (ngx_strcmp(value[1].data, "auto") == 0) {
src/core/nginx.c:1566:    if (ngx_strcmp(value[1].data, "auto") == 0) {
...

Mở src/core/nginx.c tìm "auto" thấy:

static char *
ngx_set_worker_processes(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_str_t        *value;
    ngx_core_conf_t  *ccf;

    ccf = (ngx_core_conf_t *) conf;

    if (ccf->worker_processes != NGX_CONF_UNSET) {
        return "is duplicate";
    }

    value = cf->args->elts;

    if (ngx_strcmp(value[1].data, "auto") == 0) {
        ccf->worker_processes = ngx_ncpu;
        return NGX_CONF_OK;
    }

    ccf->worker_processes = ngx_atoi(value[1].data, value[1].len);

    if (ccf->worker_processes == NGX_ERROR) {
        return "invalid value";
    }

    return NGX_CONF_OK;
}

nếu đọc từ config được giá trị là "auto", NGINX sẽ gán ccf->worker_processes = ngx_ncpu.

Tìm ngx_ncpu:

$ grep -Rn ngx_ncpu
src/os/win32/ngx_os.h:59:extern ngx_uint_t   ngx_ncpu;
src/os/win32/ngx_win32_init.c:14:ngx_uint_t  ngx_ncpu;
src/os/win32/ngx_win32_init.c:131:    ngx_ncpu = si.dwNumberOfProcessors;
src/os/unix/ngx_os.h:79:extern ngx_int_t    ngx_ncpu;
src/os/unix/ngx_posix_init.c:13:ngx_int_t   ngx_ncpu;
src/os/unix/ngx_posix_init.c:62:    if (ngx_ncpu == 0) {
src/os/unix/ngx_posix_init.c:63:        ngx_ncpu = sysconf(_SC_NPROCESSORS_ONLN);
src/os/unix/ngx_posix_init.c:67:    if (ngx_ncpu < 1) {
src/os/unix/ngx_posix_init.c:68:        ngx_ncpu = 1;
src/os/unix/ngx_freebsd_init.c:210:        ngx_ncpu = ngx_freebsd_hw_ncpu / 2;
src/os/unix/ngx_freebsd_init.c:213:        ngx_ncpu = ngx_freebsd_hw_ncpu;
src/os/unix/ngx_darwin_init.c:164:    ngx_ncpu = ngx_darwin_hw_ncpu;
...

thấy trên các hệ điều hành, NGINX sẽ lấy giá trị theo cách khác nhau. Trên "posix" như các Linux-based OS, NGINX gọi C function sysconf(_SC_NPROCESSORS_ONLN).

man sysconf

NAME
       sysconf - get configuration information at run time

SYNOPSIS
       #include <unistd.h>
...
- _SC_NPROCESSORS_ONLN
      The number of processors currently online (available).  See also get_nprocs_conf(3).

Viết 1 chương trình C 5 dòng để in ra giá trị này:

// main.c
#include <stdio.h>
#include <unistd.h>

int main() {
    printf("online CPUs %ld\n", sysconf(_SC_NPROCESSORS_ONLN));
}

// $ cc main.c  # compile C code to a.out file
// $ ./a.out
// online CPUs 4

Dùng strace xem sysconf thực sự đọc từ đâu:

$ strace ./a.out 2>&1 | grep open
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 3
$ cat /sys/devices/system/cpu/online
0-3

Vậy NGINX lấy giá trị được tính bởi sysconf, đọc từ file /sys/devices/system/cpu/online.

Trong container, NGINX auto dùng mấy worker process?

Nếu dùng podman xem link này để có thể dùng option --cpus, docker không cần chỉnh gì:

--cpus float                               Number of CPUs. The default is 0.000 which means no limit

Trong container

$ podman run --cpus=1 -it docker.io/nginx bash

#root@1f334076d74f:/ nginx &
[1] 2
2024/08/28 13:19:04 [notice] 2#2: using the "epoll" event method
2024/08/28 13:19:04 [notice] 2#2: nginx/1.27.1
2024/08/28 13:19:04 [notice] 2#2: built by gcc 12.2.0 (Debian 12.2.0-14)
2024/08/28 13:19:04 [notice] 2#2: OS: Linux 6.8.0-40-generic
2024/08/28 13:19:04 [notice] 2#2: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2024/08/28 13:19:04 [notice] 3#3: start worker processes
2024/08/28 13:19:04 [notice] 3#3: start worker process 4
2024/08/28 13:19:04 [notice] 3#3: start worker process 5
2024/08/28 13:19:04 [notice] 3#3: start worker process 6
2024/08/28 13:19:04 [notice] 3#3: start worker process 7

[1]+  Done                    nginx
# apt update && apt install -y procps python
...
# ps xau | grep nginx
root           3  0.0  0.0  11404  1892 ?        Ss   13:19   0:00 nginx: master process nginx
nginx          4  0.0  0.0  11872  3044 ?        S    13:19   0:00 nginx: worker process
nginx          5  0.0  0.0  11872  3044 ?        S    13:19   0:00 nginx: worker process
nginx          6  0.0  0.0  11872  3044 ?        S    13:19   0:00 nginx: worker process
nginx          7  0.0  0.0  11872  3044 ?        S    13:19   0:00 nginx: worker process
# nproc
4
# cat /sys/devices/system/cpu/online
0-3

Nội dung file này được mang từ máy host vào, vì vậy dù container được set bao nhiêu CPU thì NGINX (hay các ngôn ngữ lập trình ví dụ Python) vẫn đọc giá trị là số CPU của máy host.

# python3
Python 3.11.2 (main, Aug 26 2024, 07:20:54) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.cpu_count()
4

Kết luận

NGINX hay các ngôn ngữ lập trình khi chạy trong container đếm số CPU của máy host, không phải giá trị CPU request/limit cấp cho container.

Hết.

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

Ủng hộ tác giả 🍺

Friday, 2 August 2024

Docker image alpine/curl không phải alpine

Nếu cần chạy 1 câu lệnh curl (hay http client khác để truy cập web API) trên container, dùng image nào nhẹ nhất?

NOTE: thay podman bằng docker nếu dùng docker.

alpine rồi cài curl

alpine vốn phổ biến trong giới container vì nhẹ, nhưng không có sẵn curl, phải cài:

$ podman run -it alpine sh -c 'apk add curl>/dev/null; curl https://www.openbsd.org/robots.txt'
User-agent: *
Disallow: /cgi-bin/
Disallow: /donations.html

alpine/curl KHÔNG PHẢI alpine

Google alpine curl xem image alpine nào cài sẵn curl? thấy ngay kết quả top alpine/curl, và dùng có vẻ thành công:

$ podman run -it docker.io/alpine/curl sh -c 'curl https://www.openbsd.org/robots.txt'
Trying to pull docker.io/alpine/curl:latest...
Getting image source signatures
Copying blob 9f444ea7cf45 done
Copying blob 299588fda28b done
Copying blob c6a83fedfae6 done
Copying config d4f2de61cf done
Writing manifest to image destination
Storing signatures
User-agent: *
Disallow: /cgi-bin/
Disallow: /donations.html

NHƯNG alpine này là tên của 1 người dùng, không phải của hệ điều hành alpine. Nhờ cách đặt tên thông minh này mà tác giả đã khiến hàng trăm triệu lượt tải alpine/git

Không có ubuntu/curl

user ubuntu thuộc về tổ chức Canonical - công ty đứng sau Ubuntu chứ không phải 1 người dùng thông minh nào cả https://hub.docker.com/u/ubuntu

image chính thức của curl

https://hub.docker.com/r/curlimages/curl với hơn 1 tỷ lượt tải, based alpine

$ podman run -it docker.io/curlimages/curl cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.20.2
PRETTY_NAME="Alpine Linux v3.20"
$ podman run -it docker.io/curlimages/curl https://www.openbsd.org/robots.txt
User-agent: *
Disallow: /cgi-bin/
Disallow: /donations.html

busybox wget

busybox rất nhỏ, và có sẵn wget, wget khác curl https://daniel.haxx.se/docs/curl-vs-wget.html nhưng đủ tính năng để truy cập 1 HTTP API:

Option -O FILE Save to FILE ('-' for stdout)-q Quiet

$ podman run -it busybox wget -qO- https://www.openbsd.org/robots.txt
wget: note: TLS certificate validation not implemented
User-agent: *
Disallow: /cgi-bin/
Disallow: /donations.html

So sánh kích thước image

$ podman images
REPOSITORY                              TAG         IMAGE ID      CREATED        SIZE
docker.io/alpine/curl                   latest      d4f2de61cfdf  5 days ago     13.7 MB
docker.io/library/alpine                latest      a606584aa9aa  5 weeks ago    8.09 MB
docker.io/library/busybox               latest      65ad0d468eb1  14 months ago  4.5 MB
docker.io/curlimages/curl               latest      65019fbb78d5  2 days ago     21.9 MB

busybox nhỏ nhất. Nếu cần đủ tính năng curl, hãy dùng curlimages/curl.

Kết luận

Tránh bị "bất ngờ" vì alpine/curl hay alpine/git không đến từ alpine.

Ủng hộ tác giả 🍺

Thursday, 1 August 2024

Rust không import

Mọi ngôn ngữ lập trình hiện đại đều có cơ chế để dùng lại code. C có #include<stdio.h> thì Python, Go, Java có import. Rust không như thế, không có import, không cần import.

Rust dùng thư viện bằng path đầy đủ

Không cần import. Ví dụ sử dụng các function trong thư viện std::fs:

fn main() {
    std::fs::create_dir_all("a/b/c/d").unwrap();
    std::fs::write("a/file", "Hello").expect("Cannot write file");

    let rd = std::fs::read_dir("a").unwrap();
    for i in rd {
        let i = i.unwrap();
        if i.path().is_dir() {
            println!("Directory {}", i.file_name().to_string_lossy());
        } else if i.path().is_file() {
            println!("File {}", i.file_name().to_string_lossy());
        }
    }
}

Code tạo thư mục a chứa thư mục b chứa thư mục c chứa thư mục d, tương tự lệnh mkdir -p a/b/c/d trên Linux. Sau đó ghi ra 1 file text tên "file" trong thư mục "a" dòng chữ "Hello". Liệt kê các file trong thư mục "a" và in ra màn hình đâu là thư mục đâu là file.

Output:

File file
Directory b

Rust crate, module

std::fs::create_dir_allfull path tới function create_dir_all.

std là 1 crate /kreɪt/ - đơn vị 1 "library" trong Rust. Xem các crates tại https://crates.io/

fs là 1 module - thường tương ứng với 1 file hay 1 thư mục, xem các module trong crate std tại https://doc.rust-lang.org/std/index.html

Rust có thể gọi function create_dir_all trong fs trong std mà không cần import.

Nhưng gõ std::fs::create_dir_all dài, nên Rust có use dùng để tạo "shortcut".

use std::fs::create_dir_all;
// use std::fs::{create_dir_all, write}; viết ngắn gọn thay vì viết use cho từng function.
// use std::fs::{*}; tất cả
fn main() {
    create_dir_all("a/b/c/d").unwrap();
}

tương tự Python from math import sqrt; sqrt(4). hay from math import *; sqrt(4)

Nếu viết use std::fs thì sẽ chỉ cần gõ fs::create_dir_all là đủ. Đây là "best practice" để vừa gõ ngắn hơn, vừa biết create_dir_all thuộc về fs.

use std::fs;

fn main() {
    fs::create_dir_all("a/b/c/d").unwrap();
    fs::write("a/file", "Hello").expect("Cannot write file");

    let rd = fs::read_dir("a").unwrap();
    for i in rd {
        let i = i.unwrap();
        if i.path().is_dir() {
            println!("Directory {}", i.file_name().to_string_lossy());
        } else if i.path().is_file() {
            println!("File {}", i.file_name().to_string_lossy());
        }
    }
}

Kết luận

Rust không import, chỉ use để tạo shortcut.

Hết

Tham khảo

Ủng hộ tác giả 🍺

Tuesday, 16 July 2024

Compile Kotlin có cần JDK không?

Hay "Một mình chống lại cả internet và AI (ChatGPT, Gemini)".

Internet thường hay sai, thậm chí nhiều khi sai nhưng nghe có vẻ đúng đúng, là nền tảng cho sự "ảo tưởng" của các AI chatbot bịa ra đủ thứ và nói rất hay.

build kotlin "Hello world" có cần JDK không?

Với câu hỏi "I want to write kotlin hello world program and build with kotlinc, do I need to install JDK (not JRE)", lên mạng internet tìm câu trả lời:

Ta tìm được StackOverFlow đầy uy tín (và nổi tiếng vì nhiều câu trả lời sai)

Thử hỏi ChatGPT

chatgpt

Và AI của Google: Gemini

gemini

Mọi câu trả lời đều là "có". Nhưng... bài trước Hello Kotlin 2024 hoàn toàn không thấy nhắc tới JDK, vậy có cần không?

JRE là gì


$ apt-cache search openjdk | grep -i jdk-17
openjdk-17-doc - OpenJDK Development Kit (JDK) documentation
openjdk-17-jdk - OpenJDK Development Kit (JDK)
openjdk-17-jdk-headless - OpenJDK Development Kit (JDK) (headless)
openjdk-17-jre - OpenJDK Java runtime, using Hotspot JIT
openjdk-17-jre-headless - OpenJDK Java runtime, using Hotspot JIT (headless)
openjdk-17-jre-zero - Alternative JVM for OpenJDK, using Zero
openjdk-17-source - OpenJDK Development Kit (JDK) source files

Java Runtime Environment (JRE) chứa máy ảo JVM, dùng để chạy các file .class, hay nói chung: để chạy các chương trình trên JVM. JRE tương ứng với chương trình câu lệnh java.

JDK là gì

Java Development Kit (JDK) chứa Java compiler và nhiều thành phần khác cần thiết để viết chương trình chạy trên JVM. JDK có chương trình câu lệnh javac, khi viết bất kì chương trình Java nào đều cần javac để compile thành .class.

Kotlin compiler

Kotlin sử dụng compiler để biến code Kotlin (file .kt) thành Java bytecode (file .class), rồi chạy code đó (interpret) trên máy ảo JVM.

Java và Kotlin là điển hình cho ngôn ngữ dùng cả "compiler" lẫn "interpreter".

Kotlin compiler https://kotlinlang.org/docs/command-line.html được đóng gói thành một file JAR, và người dùng thường nhìn thấy câu lệnh "kotlinc" (thực chất là 1 script).

Kotlinc

kotlinc là một script để gọi câu lệnh java với các option cần thiết:

$ cat $(command -v kotlinc)
#!/usr/bin/env bash
#
...
# Copyright 2011-2015, JetBrains
...
    kotlin_app=("${KOTLIN_HOME}/lib/kotlin-preloader.jar" "org.jetbrains.kotlin.preloading.Preloader" "-cp" "${KOTLIN_HOME}/lib/kotlin-compiler.jar${additional_classpath}" $KOTLIN_COMPILER)
fi

"${JAVACMD:=java}" $JAVA_OPTS "${java_args[@]}" -cp "${kotlin_app[@]}" "${kotlin_args[@]}"

Có thể thấy file kotlin-compiler.jar được dùng ở script trên. Khi chạy kotlinc chỉ thấy gọi lệnh java, không thấy dùng javac (Java compiler).

Build chương trình Kotlin đơn giản dùng stdlib của Java

# main.kt
import java.math.BigInteger

fun pe16(): Int {
    val p: BigInteger = 2.toBigInteger().pow(1000)
    return p.toString()
            .chars()
            .map({ it - '0'.toInt() })
            .sum()
}

fun main() {
    println("tong cac chu so cua 2**1000 la ${pe16()}".toUpperCase())
}

Kết quả

$ kotlinc main.kt
$ kotlin MainKt
TONG CAC CHU SO CUA 2**1000 LA 1366

Hoàn toàn không thấy sử dụng javac trong quá trình này. Trên máy thậm chí không có câu lệnh javac tức không cài JDK.

$ javac
zsh: command not found: javac
$ dpkg -l | grep
ii  openjdk-11-jre:amd64                             11.0.23+9-1ubuntu1~22.04.1                        amd64        OpenJDK Java runtime, using Hotspot JIT
ii  openjdk-11-jre-headless:amd64                    11.0.23+9-1ubuntu1~22.04.1                        amd64        OpenJDK Java runtime, using Hotspot JIT (headless)

Kết luận

Build chương trình kotlin không cần JDK.

Sau khi lên kotlinlang Slack để kiểm tra lại câu trả lời, một người dùng Kotlin lâu năm đã cùng sửa lại internet với câu trả lời StackOverFlow https://stackoverflow.com/a/78750618/807703.

Kết luận

Thời buổi AI 4.0, nhớ giành thời gian tự suy nghĩ.

Hết.

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

Ủng hộ tác giả 🍺

Saturday, 13 July 2024

Hello Kotlin 2024

Kotlin là gì

kotlin

$ apt-cache show kotlin
Package: kotlin
Architecture: all
Version: 1.3.31+~1.0.1+~0.11.12-2
...
Installed-Size: 286240
...
Homepage: https://kotlinlang.org
Description-en: cross-platform, general-purpose programming languagehttps://kotlinlang.org/
 Kotlin is a cross-platform, statically typed, general-purpose programming
 language with type inference. Kotlin is designed to interoperate fully with
 Java, and the JVM version of its standard library  depends on the Java Class
 Library, but type inference allows its syntax to be more concise.

Kotlin https://kotlinlang.org/ là một ngôn ngữ lập trình static-typing hiện đại (2011) phát triển bởi JetBrain (IDE: PyCharm, IntelliJ IDEA), chạy trên máy ảo JVM (của Java), tương thích với Java (dùng được thư viện Java), cú pháp ngắn gọn, hỗ trợ functional programming, được Google hỗ trợ là ngôn ngữ chính thức để viết app Android.

Tại sao dùng Kotlin?

Để hiểu 1 hệ thống đầy đủ, lập trình viên cần hiểu từ frontend (FE) tới backend (BE). Lập tức nghĩ rằng JavaScript FE + NodeJS BE là câu trả lời? Câu trả lời này đúng, nhưng không phải duy nhất. Ngày nay, đa phần các sản phẩm đều có frontend là mobile app, nên app Android hay iOS cũng là các lựa chọn cho frontend, nhiều ứng dụng thậm chí không có hoặc có mà ít dùng web frontend (Grab/Uber/Tiktok/Momo). Ngoài ra các hệ thống doanh nghiệp đa số sử dụng Java hay .NET cho BE chứ không phải NodeJS. Khiến cho các option là:

  • Java/Kotlin BE, Java/Kotlin mobile FE
  • BE nào đó, Object-C/Swift iOS mobile FE
  • BE nào đó, JavaScript web FE
  • BE nào đó, ReactNative/Flutter mobile FE

Java/Kotlin là option ngon lành nhất ở đây để làm cả FE lẫn BE.

Cài đặt

$ sudo apt install -y kotlin
...
Setting up kotlin (1.3.31+~1.0.1+~0.11.12-2) ...

Viết hello world

Viết code vào file main.kt:

fun main() {
    println("Hello FAMILUG 2024!")
}

Build source code thành file MainKt.class

$ kotlinc main.kt
$ kotlin MainKt
Hello FAMILUG 2024!
$ find .
.
./META-INF
./META-INF/main.kotlin_module
./MainKt.class
./main.kt

$ file MainKt.class
MainKt.class: compiled Java class data, version 50.0 (Java 1.6)

$ xxd MainKt.class| head -n3
00000000: cafe babe 0000 0032 002b 0100 064d 6169  .......2.+...Mai
00000010: 6e4b 7407 0001 0100 106a 6176 612f 6c61  nKt......java/la
00000020: 6e67 2f4f 626a 6563 7407 0003 0100 046d  ng/Object......m

8 bytes đầu đáng yêu của file .class: "cafe babe".

Chạy file MainKt

$ /usr/bin/time -v kotlin MainKt
Hello FAMILUG 2024!
    Command being timed: "kotlin MainKt"
    User time (seconds): 0.08
    System time (seconds): 0.02
    Percent of CPU this job got: 116%
    Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.10
    Average shared text size (kbytes): 0https://github.com/JetBrains/kotlin/releases/tag/v1.3.31
    Average unshared data size (kbytes): 0
    Average stack size (kbytes): 0
    Average total size (kbytes): 0
    Maximum resident set size (kbytes): 37888
    Average resident set size (kbytes): 0
    Major (requiring I/O) page faults: 0
    Minor (reclaiming a frame) page faults: 5296
    Voluntary context switches: 282
    Involuntary context switches: 13
    Swaps: 0
    File system inputs: 0
    File system outputs: 64
    Socket messages sent: 0
    Socket messages received: 0
    Signals delivered: 0
    Page size (bytes): 4096
    Exit status: 0

Dùng tới ~37 MiB, gấp 4 lần RAM so với Python, nhưng không sao, năm 2024 thì 40MB RAM không là gì so với Google Chrome hay các IDE cả.

$ /usr/bin/time -v /usr/bin/python3.11 -c 'print("Hello FAMILUG!")' |& grep Maximum
    Maximum resident set size (kbytes): 8960

Viết vài chương trình đơn giản

Viết function tính tổng 3 số

fun sum3(a: Int, b: Int, c: Int): Int {
    return a + b + c
}

fun main() {
    println("Hello FAMILUG 2024!")
    val sum = sum3(1,2,3)
    println("Tong cua 1+2+3 la ${sum}")
}

Kết quả:

$ kotlinc main.kt
$ kotlin MainKt
Hello FAMILUG 2024!
Tong cua 1+2+3 la 6

Không khác mấy code Python3

def sum3(a: int, b: int, c: int) -> int:
    return a + b +c

def main():
    print("Hello FAMILUG 2024!")
    sum = sum3(1,2,3)
    print(f"Tong cua 1+2+3 la {sum}")

main()

Giải bài ProjectEuler 1 - tổng các số nhỏ hơn 1000 chia hết cho 3 hoặc 5

# main.kt
fun pe01(): Int {
    var sum = 0
    for (i in 1..999) {
        if (i % 3 == 0 || i % 5 == 0) {
            sum += i
        }
    }
    return sum
}

fun pe01_functional(): Int {
    return (1..999)
        .filter({it -> it % 3 == 0 || it % 5 == 0}) // hoặc ngắn hơn: .filter({ it % 3 == 0 || it % 5 == 0 })
        .sum()
}


fun main() {
    println(pe01())
    println(pe01() == pe01_functional())
}

Kết quả

$ kotlinc main.kt
hvn@mini:kotlinplay $ kotlin MainKt
233168
true

Giải bài ProjectEuler 16 - tổng các chữ số của 2 mũ 1000

# main.kt
import java.math.BigInteger

fun pe16(): Int {
    val p: BigInteger = 2.toBigInteger().pow(1000)
    return p.toString()
            .chars()
            .map({ it - '0'.toInt() })
            .sum()
}

fun main() {
    println("tong cac chu so cua 2**1000 la ${pe16()}".toUpperCase())
}

Kết quả

$ kotlinc main.kt
$ kotlin MainKt
TONG CAC CHU SO CUA 2**1000 LA 1366

String functions: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/ Char functions: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-char/

Đọc file /etc/passwd và tìm max uid trong các user

Dùng sort command để so sánh kết quả:

$ sort -nk 3 /etc/passwd | head -n3
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
avahi-autoipd:x:110:119:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin
avahi:x:114:121:Avahi mDNS daemon,,,:/run/avahi-daemon:/usr/sbin/nologin

Code Kotlin

import java.io.File
import java.io.InputStream

fun main() {
    val inputStream: InputStream = File("/etc/passwd").inputStream()
    val lineList = mutableListOf<Int>()

    inputStream.bufferedReader().forEachLine(
        { it -> lineList.add(it.split(":")[2].toInt()) }
    )
    println(lineList.max())
}

Kết quả

$ kotlinc main.kt
$ kotlin MainKt
65534

Tham khảo

Kotlin tour: https://kotlinlang.org/docs/kotlin-tour-hello-world.html

Tổng kết

Bài này giới thiệu Kotlin và viết các chương trình đơn giản, sử dụng các thư viện standard/thư viện có sẵn của Java, đồng thời dùng bản Kotlin có sẵn trên Ubuntu 22.04 đã khá cũ (1.3.31 từ 2019) bản hiện tại là Kotlin 2.0, build và chạy code trực tiếp với kotlinc trên câu lệnh CLI mà không dùng IDE hay build tool.

Bài viết sau sẽ giới thiệu build tool, sử dụng kotlin bản mới nhất và các thư viện tải từ trên mạng.

Kết luận

Kotlin ngắn gọn, hiện đại, viết được app di động lẫn code backend/frontend. Ngon không thể bỏ phi.

Hết.

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

Ủng hộ tác giả 🍺

Friday, 12 July 2024

Chơi CTF giúp gì cài OpenSearch?

OpenSearch vs ElasticSearch

OpenSearch là 1 bản fork của Amazon từ ElasticSearch, sau vụ đổi license đình đám từ công ty Elastic đứng sau ElasticSearch không muốn Amazon thu lời từ sản phẩm open-source của họ. OpenSearch ngày càng khác biệt với ElasticSearch, mang theo những ưu / nhược điểm riêng.

Một nhược điểm là nó fork từ bản cũ của ElasticSearch, nên nhiều lỗi còn rất khó đọc. Bài này nhờ kỹ năng chơi CTF với team PyMi mà giải quyết một vấn đề đau đầu.

Một ưu điểm là OpenSearch hỗ trợ giải pháp login doanh nghiệp bằng SSO: SAML, OIDC, LDAP... còn ElasticSearch phải trả phí mua license.

Chạy 1 node trên máy bằng podman (docker)

podman là 1 phần mềm phát triển bởi RedHat thay thế cho docker, sau khi docker đổi qua tính phí trên Desktop (DockerDesktop), podman dần trở nên phổ biến. Câu lệnh podman tương thích với docker nên chỉ cần thay chữ podman trong bài thành docker là được. Bài này chạy bằng podman trên máy để bạn đọc có thể làm theo, thực tế chạy trên 1 K8S cluster

Chạy 1 container opensearch theo hướng dẫn tại trang dockerhub, thấy rất nhiều log:

$ podman run -it -p 9200:9200 -p 9600:9600 -e OPENSEARCH_INITIAL_ADMIN_PASSWORD=daemonH4gx@4 -e "discovery.type=single-node" --name opensearch-node docker.io/opensearchproject/opensearch:2.15.0

Enabling OpenSearch Security Plugin
Enabling execution of install_demo_configuration.sh for OpenSearch Security Plugin
OpenSearch 2.12.0 onwards, the OpenSearch Security Plugin a change that requires an initial password for 'admin' user.
Please define an environment variable 'OPENSEARCH_INITIAL_ADMIN_PASSWORD' with a strong password string.
If a password is not provided, the setup will quit.
 For more details, please visit: https://opensearch.org/docs/latest/install-and-configure/install-opensearch/docker/
### OpenSearch Security Demo Installer
### ** Warning: Do not use on production or public reachable systems **
OpenSearch install type: rpm/deb on Linux 6.5.0-41-generic amd64
OpenSearch config dir: /usr/share/opensearch/config/
OpenSearch config file: /usr/share/opensearch/config/opensearch.yml
OpenSearch bin dir: /usr/share/opensearch/bin/
OpenSearch plugins dir: /usr/share/opensearch/plugins/
OpenSearch lib dir: /usr/share/opensearch/lib/
Detected OpenSearch Version: 2.15.0
Detected OpenSearch Security Version: 2.15.0.0
Admin password set successfully.
### Success
...
[2024-07-12T13:16:30,697][INFO ][o.o.s.c.ConfigurationRepository] [00a159b488ac] Node '00a159b488ac' initialized

server đã mặc định setup sẵn SSL/TLS port 9200.

Sau khi cài lên trên K8S cluster, bỗng dưng xuất hiện hàng đống log error như sau

[2024-07-12T13:16:36,456][ERROR][o.o.h.n.s.SecureNetty4HttpServerTransport] [00a159b488ac] Exception during establishing a SSL connection: io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record: 474554202f20485454502f312e310d0a486f73743a206c6f63616c686f73743a393230300d0a557365722d4167656e743a206375726c2f372e38312e300d0a4163636570743a202a2f2a0d0a0d0a
io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record: 474554202f20485454502f312e310d0a486f73743a206c6f63616c686f73743a393230300d0a557365722d4167656e743a206375726c2f372e38312e300d0a4163636570743a202a2f2a0d0a0d0a
    at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1314) ~[netty-handler-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1387) ~[netty-handler-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:530) ~[netty-codec-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:469) ~[netty-codec-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290) ~[netty-codec-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) [netty-transport-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) [netty-transport-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) [netty-transport-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1407) [netty-transport-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440) [netty-transport-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) [netty-transport-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918) [netty-transport-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) [netty-transport-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788) [netty-transport-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysPlain(NioEventLoop.java:689) [netty-transport-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:652) [netty-transport-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) [netty-transport-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:994) [netty-common-4.1.110.Final.jar:4.1.110.Final]
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) [netty-common-4.1.110.Final.jar:4.1.110.Final]
    at java.base/java.lang.Thread.run(Thread.java:1583) [?:?]

dễ dàng tìm thấy lỗi này trên mạng

nhưng ai gửi request http đến địa chỉ này?

Tìm ai gửi request

Các phương án:

  • cài tcpdump, ngrep để dump traffic pod 9200 xem nội dung traffic, không dễ cài được package nếu image đã remove các package manager (như dùng distroless images)
  • đọc log?! ai đọc stacktrace Java???

Giải pháp là đọc log, phần nội dung bí hiểm trong log message:

474554202f20485454502f312e310d0a486f73743a206c6f63616c686f73743a393230300d0a557365722d4167656e743a206375726c2f372e38312e300d0a4163636570743a202a2f2a0d0a0d0a

chứa dãy 45 47 55 ... các ký tự từ a-e từ 0-9... với kinh nghiệm chơi các bài dễ trong các giải CTF, đoán ngay dùng hex https://n.pymi.vn/base16.html để xem là gì. Bật Python:

>>> s = "474554202f20485454502f312e310d0a486f73743a206c6f63616c686f73743a393230300d0a557365722d4167656e743a206375726c2f372e38312e300d0a4163636570743a202a2f2a0d0a0d0a"
>>> bytes.fromhex(s)
b'GET / HTTP/1.1\r\nHost: localhost:9200\r\nUser-Agent: curl/7.81.0\r\nAccept: */*\r\n\r\n'

đọc được nội dung của request đã gửi tới server.

Trên thực tế, đống log có User-Agent là 1 chương trình monitoring, từ đó tìm ra và tắt nó đi.

PS: có thể dùng https://gchq.github.io/CyberChef/ để decode hex.

Kết luận

Chơi CTF không bổ ngang thì bổ dọc, không bổ dọc thì lại bổ ngang.

Thực hiện trên:

$ lsb_release -d
Description:    Ubuntu 22.04.4 LTS

$ podman --version
podman version 3.4.4

Hết.

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

Ủng hộ tác giả 🍺