Skip to main content

Đọc code Go Traefik xem router ưu tiên match theo độ dài rule

Traefik là gì

Traefik (pronounced traffic) is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy. Traefik integrates with your existing infrastructure components (Docker, Swarm mode, Kubernetes, Consul, Etcd, Rancher v2, Amazon ECS, ...) and configures itself automatically and dynamically. Pointing Traefik at your orchestrator should be the only configuration step you need.

Traefik là một HTTP reverse proxy và load balancer giúp triển khai microservices dễ dàng. Traefik có tính năng tương tự NGINX, với điểm mạnh lớn nhất là khả năng tích hợp, cấu hình tự động với hệ thống đã chạy như Kubernetes, Docker, ...

Traefik được viết bằng Go, về hiệu năng chưa có gì vượt trội so với NGINX, nhưng có nhiều tính năng hiện đại như: tích hợp HTTPS dễ dàng, hỗ trợ sẵn dashboard, metrics, rate limit,...

Build Traefik từ source

Tạo file build.sh

git clone https://github.com/traefik/traefik --depth 1 --branch v3.5
cd traefik
touch webui/static/index.html
make dist
/usr/bin/time -v make binary
./dist/linux/amd64/traefik version

Chạy để build với go1.24:

bash build.sh
Cloning into 'traefik'...
remote: Enumerating objects: 2215, done.
remote: Counting objects: 100% (2215/2215), done.
remote: Compressing objects: 100% (1713/1713), done.
remote: Total 2215 (delta 654), reused 1107 (delta 427), pack-reused 0 (from 0)
Receiving objects: 100% (2215/2215), 12.89 MiB | 9.15 MiB/s, done.
Resolving deltas: 100% (654/654), done.
fatal: No names found, cannot describe anything.
mkdir -p dist
fatal: No names found, cannot describe anything.
SHA: 27a820950a316451da3d52091cdfac7138e4ee8a cheddar 2025-09-14_04:31:45AM
CGO_ENABLED=0 GOGC= GOOS=linux GOARCH=amd64 go build  -ldflags "-s -w \
    -X github.com/traefik/traefik/v3/pkg/version.Version=27a820950a316451da3d52091cdfac7138e4ee8a \
    -X github.com/traefik/traefik/v3/pkg/version.Codename=cheddar \
    -X github.com/traefik/traefik/v3/pkg/version.BuildDate=2025-09-14_04:31:45AM" \
    -installsuffix nocgo -o "./dist/linux/amd64/traefik" ./cmd/traefik
    Command being timed: "make binary"
    User time (seconds): 5.59
    System time (seconds): 1.02
    Percent of CPU this job got: 150%
    Elapsed (wall clock) time (h:mm:ss or m:ss): 0:04.40
    Maximum resident set size (kbytes): 2106624
    ...
    Exit status: 0
Version:      27a820950a316451da3d52091cdfac7138e4ee8a
Codename:     cheddar
Go version:   go1.24.0
Built:        2025-09-14_04:31:45AM
OS/Arch:      linux/amd64

Sửa code router in ra chi tiết priority

Thêm 1 dòng print

diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go
index 44f6950..12c6589 100644
--- a/pkg/server/router/router.go
+++ b/pkg/server/router/router.go
@@ -142,6 +142,8 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName str
                        routerConfig.Priority = httpmuxer.GetRulePriority(routerConfig.Rule)
                }

+               fmt.Printf("FML priority name: %v rule: '%s' len: %d\n", routerName, routerConfig.Rule, routerConfig.Priority)

                if routerConfig.Priority > maxUserPriority && !strings.HasSuffix(routerName, "@internal") {
                        err = fmt.Errorf("the router priority %d exceeds the max user-defined priority %d", routerConfig.Priority, maxUserPriority)
                        routerConfig.AddError(err, true)

rồi build lại.

Cấu hình Prometheus metrics cho Traefik

# traefik.yml
entryPoints:
  web:
    address: :2280
log:
  level: DEBUG
accessLog:
  addInternals: true
metrics:
  addInternals: true
  prometheus:
    manualRouting: true
    entryPoint: web
providers:
  file:
    filename: ./traefik_provider.yml
    watch: true
# traefik_provider.yml
http:
  routers:
    router1:
      entryPoints:
        - web
      service: prometheus@internal
      rule: PathPrefix(`/metrics`)
      observability:
        accessLogs: true
    regexrouter2:
      entryPoints:
        - web
      service: prometheus@internal
      rule: PathRegexp(`/me.*`)
      observability:
        accessLogs: true

Chạy:

 ./traefik/dist/linux/amd64/traefik --configfile traefik.yml
2025-09-14T11:40:01+07:00 DBG traefik/cmd/traefik/traefik.go:114 > Static configuration loaded [json] staticConfiguration={"accessLog":{"addInternals":true,"fields":{"defaultMode":"keep","headers":{"defaultMode":"drop"}},"filters":{},"format":"common"},"entryPoints":{"web":{"address":":2280","forwardedHeaders":{},"http":{"maxHeaderBytes":1048576,"sanitizePath":true},"http2":{"maxConcurrentStreams":250},"transport":{"lifeCycle":{"graceTimeOut":"10s"},"respondingTimeouts":{"idleTimeout":"3m0s","readTimeout":"1m0s"}},"udp":{"timeout":"3s"}}},"global":{"checkNewVersion":true},"log":{"format":"common","level":"DEBUG"},"metrics":{"addInternals":true,"prometheus":{"addEntryPointsLabels":true,"addServicesLabels":true,"buckets":[0.1,0.3,1.2,5],"entryPoint":"web","manualRouting":true}},"providers":{"file":{"filename":"./traefik_provider.yml","watch":true},"providersThrottleDuration":"2s"},"serversTransport":{"maxIdleConnsPerHost":200},"tcpServersTransport":{"dialKeepAlive":"15s","dialTimeout":"30s"}}
...
FML priority name: router1@file rule: 'PathPrefix(`/metrics`)' len: 22
...
FML priority name: regexrouter2@file rule: 'PathRegexp(`/me.*`)' len: 19
...
127.0.0.1 - - [14/Sep/2025:04:40:06 +0000] "GET /metrics HTTP/1.1" 200 2065 "-" "-" 1 "router1@file" "-" 1ms
127.0.0.1 - - [14/Sep/2025:04:41:31 +0000] "GET /metrics_but_longer HTTP/1.1" 200 2057 "-" "-" 1 "router1@file" "-" 1ms

Traefik ưu tiên các route có rule dài hơn

Khi truy cập curl localhost:2280/metrics_but_longer thấy Traefik log đã match router1@file:

  • router1 chứa rule có độ dài 22 ký tự nên được ưu tiên match trước.
  • regexrouter2 chứa rule chỉ có 19 kí tự

To avoid path overlap, routes are sorted, by default, in descending order using rules length. The priority is directly equal to the length of the rule, and so the longest length has the highest priority. source

Trong code https://github.com/traefik/traefik/blob/27a820950a316451da3d52091cdfac7138e4ee8a/pkg/muxer/http/mux.go#L73-L88 thực hiện sort các router theo priority:

// AddRoute add a new route to the router.
func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler http.Handler) error {
    matchers, err := m.parser.parse(syntax, rule)
    if err != nil {
        return fmt.Errorf("error while parsing rule %s: %w", rule, err)
    }

    m.routes = append(m.routes, &route{
        handler:  handler,
        matchers: matchers,
        priority: priority,
    })

    sort.Sort(m.routes)

    return nil
}
...
// routes implements sort.Interface.
type routes []*route

// Len implements sort.Interface.
func (r routes) Len() int { return len(r) }

// Swap implements sort.Interface.
func (r routes) Swap(i, j int) { r[i], r[j] = r[j], r[i] }

// Less implements sort.Interface.
func (r routes) Less(i, j int) bool { return r[i].priority > r[j].priority }

Sort interface sắp xếp theo thứ tự tăng dần:

Sort sorts data in ascending order as determined by the Less method. It makes one call to data.Len to determine n and O(n*log(n)) calls to data.Less and data.Swap. The sort is not guaranteed to be stable.

Nhưng function Less ở đây trả về true khi phần tử r[i].priority > r[j].priority, tức priority càng cao thì xem như càng đứng trước, vì vậy route có priority cao nhất sẽ đứng đầu.

Khi ServeHTTP nhận HTTP request, nó duyệt qua các routes, route có priority cao hơn được duyệt trước:

// ServeHTTP forwards the connection to the matching HTTP handler.
// Serves 404 if no handler is found.
func (m *Muxer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
    ...
    for _, route := range m.routes {
        if route.matchers.match(req) {
            route.handler.ServeHTTP(rw, req)
            return
        }
    }

    m.defaultHandler.ServeHTTP(rw, req)
}

Một ngày đẹp trời mà xấu số, người dùng thêm điều kiện kiểm tra Host vào regexrouter2:

      rule: PathRegexp(`/me.*`) && Host(`localhost`)

vô tình khiến route regexrouter2 có priority là 40 và được match trước:

> FML priority name: regexrouter2@file rule: 'PathRegexp(`/me.*`) && Host(`localhost`)' len: 40
...
> 127.0.0.1 - - [14/Sep/2025:08:26:40 +0000] "GET /metrics_but_longer HTTP/1.1" 200 8682 "-" "-" 1 "regexrouter2@file" "-" 0ms

Kết luận

Người dùng nên chỉ định priority cụ thể cho từng route trong config để tránh bất ngờ khi thay đổi config tưởng chừng vô hại lại có thể đảo lộn thứ tự match route:

http:
  routers:
    router1:
      entryPoints:
        - web
      service: prometheus@internal
      rule: PathPrefix(`/metrics`)
      priority: 200
    regexrouter2:
      entryPoints:
        - web
      service: prometheus@internal
      rule: PathRegexp(`/me.*`) && Host(`localhost`)
      priority: 100

Hết.

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

Ủng hộ tác giả 🍺

Comments

Popular posts from this blog

Tài liệu và hướng dẫn học Python

Để tiết kiệm thời gian, tốt nhất là đi học PyMI Updated: 130617 Sau đây là các tài liệu khuyên dùng: Vì nhiều lý do, nên học python2.7 tại thời điểm hiện tại (giờ là tháng 6/2013 - muốn biết tại sao thì tự tìm hiểu) python 3.5+ (giờ là tháng 2/2017) Chuẩn bị: 1. biết bật tắt máy 2. biết cài python 3. tập gõ 10 ngón - gõ 2 ngón hay 1 ngón cũng không sao, nhưng 10 ngón là cách dễ nhất để gõ nhanh nhất. Tài liệu - Nên dùng tài liệu tại trang chủ của Python làm chính, tham khảo thêm các tài liệu khác tại http://www.familug.org/2016/12/free-ebook.html Căn bản, mới học 1.1 Python PyMI.vn https://pymi.vn/tutorial/ 1.2. Python offical tutorial kết hợp làm bài tập trên HackerRank  (đề bài bằng tiếng Anh, nhưng Google translate 1 lúc cũng ra vì có nhiều ví dụ mẫu đi kèm). Học viên của Pymi.vn có rất nhiều học viên đã tự học với Learn Python the hard way nhưng chưa thấy ai thành công cả. Hai link dưới nên đọc sau khi đã nắm được những phần cơ bản của ngôn ngữ pytho...

The PyMiers

New FAMILUG