Skip to content
EngineeringOctober 27, 2026

Chúng tôi đã làm AnyRouter nhanh hơn thế nào

Đầu tháng 10, một request đã ấm có thể chờ tới 1,7 giây mà bộ đếm thời gian của chính nhà cung cấp model không hề tính. Gần như toàn bộ thời gian đó không phải là xử lý. Đó là một chuỗi lệnh đọc nhỏ, lệnh sau chờ lệnh trước. Bài này kể cách chúng tôi tìm ra nó, những gì đã đổi, bản sửa phải revert, và số liệu trước và sau.

Hai chiếc đồng hồ trong mỗi request

Khi bạn gọi một model qua AnyRouter, thời gian chờ có hai phần. Phần đầu là của chúng tôi. Chúng tôi kiểm tra key, chọn upstream và chuyển request đi. Phần thứ hai thuộc về model. Nhà cung cấp xếp hàng request, đọc prompt và sinh token.

Chúng tôi không thể làm nhà cung cấp sinh token nhanh hơn. Phần của chúng tôi thì khác. Request nào cũng phải trả phần này, một vòng lặp agent trả nó hàng trăm lần, và toàn bộ nó chạy trong code của chúng tôi.

Bài viết How we made claude.ai faster của Anthropic nhấn mạnh rằng phải đo trước rồi mới tối ưu. Chúng tôi làm theo thứ tự đó. Bài này nói về phần việc trong gateway và các trang không còn chạy code cho mỗi lượt xem. Mọi con số đều từ phép đo của chính chúng tôi, và mỗi con số đều nói rõ cách đo.

Bước một: làm cho thời gian của chúng tôi nhìn thấy được

Trước ngày 2 tháng 10, một request được phục vụ chỉ báo độ trễ của nhà cung cấp. Thời gian nằm trong gateway trước khi gửi đi không được đo ở đâu cả. Vì vậy thay đổi đầu tiên là một phép đo, chưa phải một bản sửa.

Mọi response inference giờ có header chuẩn Server-Timing. gw là thời gian từ lúc request tới đến lần gọi upstream đầu tiên. upstream là thời gian của mỗi lần gọi nhà cung cấp, mỗi lần một mục. post là thời gian từ lúc gửi xong đến lúc response được dựng xong. total là tổng. gw lại được chia thành các giai đoạn, như gw-auth (kiểm tra key), gw-ctx (ngữ cảnh tài khoản), gw-prep và gw-route (chọn upstream). Tổng các giai đoạn luôn bằng gw.

Chỉ in header timing của một chat completion
curl -s -o /dev/null -D - https://anyrouter.dev/api/v1/chat/completions \
  -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"anyrouter/free","messages":[{"role":"user","content":"hi"}]}' \
  | grep -i server-timing

Một lần gọi khi viết bài này trả về dòng dưới đây. Chúng tôi đã bỏ mục theo từng lần gọi, vì nó ghi tên backend.

server-timing: gw;dur=764, gw-auth;dur=222, gw-ctx;dur=238, gw-prep;dur=23, gw-route;dur=281, upstream;dur=5487, post;dur=10, total;dur=6261

Ở đây model mất 5,5 giây và chúng tôi mất 0,76 giây. Con số này cao hơn nhiều so với số khi ấm ở phần sau. Đó là lần gọi đầu tiên sau một lúc, nên chúng tôi đoán cache của isolate đang nguội. Hai giai đoạn 222 ms và 238 ms khớp với mẫu khi nguội được mô tả bên dưới.

Phép đo đầu tiên cho thấy gì

Ngày 2 tháng 10, chúng tôi so thời gian phía client với độ trễ do chính nhà cung cấp báo, trên các route một hop, từ một isolate đã ấm. Request lỗi trước khi định tuyến là nhóm đối chứng. Nó chỉ trả một vòng mạng.

RouteThời gian phía clientĐộ trễ nhà cung cấpChưa quy cho ai
anyrouter/auto0,97 s59 mskhoảng 0,9 s
anyrouter/agent1,81 s50 mskhoảng 1,7 s
anyrouter/coding1,56 s81 mskhoảng 1,5 s
404, không định tuyến (đối chứng)0,19 skhông cókhoảng 0,19 s (mạng)

Model trả lời trong 50 đến 81 ms. Chúng tôi mất gần một giây, có lúc gần hai giây, trước khi gọi nó.

Chi phí là chờ, không phải làm

Ban đầu chúng tôi nghi phần việc sau response, như tính tiền và ghi log. Không phải vậy. Phần đó vốn đã chạy sau khi response được gửi.

Chi phí nằm trước khi gửi đi. Một request đi qua một chuỗi lệnh đọc, lần lượt từng cái. Nó đọc mục key, giữ credit trong database, tải ngữ cảnh tài khoản, gọi hai bộ đếm rate limit, rồi chạy từng bước kiểm tra của bộ định tuyến. Mỗi lệnh đọc đều nhanh. Cộng lại là 1 đến 2 giây chờ.

Phần lớn các lệnh đọc đó không phụ thuộc nhau. Vì vậy bản sửa là một quy tắc. Hai lệnh đọc không phụ thuộc nhau thì không được chờ nhau.

Sau

Đã parse request

Key

Tài khoản

Rate limit

Kiểm tra route

Gửi đi

Trước

Key

Tài khoản

Rate limit

Kiểm tra route, từng cái

Gửi đi

Trước đây, mỗi lệnh đọc độc lập chờ lệnh trước. Bây giờ, các lệnh đọc độc lập được gửi cùng lúc, và request chỉ chờ một lần cho lệnh chậm nhất.

Đây là các mẫu chúng tôi tìm thấy và loại bỏ:

  • Các router model như anyrouter/auto mở rộng từng nhánh ứng viên một. Một request có thể chạy tới 8 pipeline định tuyến đầy đủ nối tiếp nhau, cộng thêm 8 cái dự phòng. Các nhánh độc lập với nhau, nên giờ chúng được mở rộng song song theo lô 8 cái và ghép lại theo đúng thứ tự cũ.
  • Danh sách model bị tắt được đọc lại ở mỗi lần gọi. Bộ định tuyến hỏi một lần cho mỗi nhánh, nên một request có thể đọc nó tới 16 lần. Giờ nó được cache theo isolate, và mỗi lần ghi sẽ làm mới cache.
  • Một cache lẽ ra chỉ sống trong một request lại bị tạo mới cho từng nhánh, nên nhánh nào cũng truy vấn lại database. Giờ request truyền một cache duy nhất cho mọi nhánh.
  • Các bước kiểm tra sức khoẻ upstream chạy tuần tự, và cờ quarantine được đọc bằng tối đa 4 lệnh mỗi ứng viên. Giờ chúng được gửi thành một đợt.
  • Hai bộ đếm rate limit, thông tin xác thực nhà cung cấp và các bước kiểm tra danh tính tài khoản giờ đều được gửi cùng lúc thay vì lần lượt.

Không thay đổi nào ở trên làm đổi quyết định định tuyến. Các test kiểm tra rằng thứ tự ứng viên và mọi bản ghi bỏ qua vẫn giữ nguyên.

Con số đã thay đổi

Chúng tôi đo gw bằng key smoke test với anyrouter/free, từ một máy test cố định. Ấm nghĩa là cache của isolate đã được nạp. Sau khi nghỉ nghĩa là request tới sau khoảng 100 giây không có traffic, khi các cache đó đã hết hạn.

NgàyTrạng thái codegw khi ấmgw sau khi nghỉ
3 tháng 10Khi các lệnh đọc song song đang được đưa lên364 đến 697 ms2.254 đến 2.306 ms
12 tháng 10Đọc song song đã chạy, trước bước làm ấm cache bên dưới68 đến 85 ms (p50)1.269 đến 1.652 ms

Hãy xem đây là xu hướng, không phải một phép thử A/B sạch. Hai dòng đến từ hai script khác nhau, cách nhau chín ngày, mỗi dòng chỉ vài mẫu. Một phần lệnh đọc song song có thể đã chạy từ ngày 3 tháng 10. Key smoke test cũng bỏ qua rate limit, nên gw của người dùng thật sẽ cao hơn các số này một chút.

gw khi ấm giảm từ 364 đến 697 ms xuống 68 đến 85 ms. Đường sau khi nghỉ vẫn tốn hơn một giây, và đó là vấn đề tiếp theo.

Vách đá khi nguội, và bản sửa phải revert

Sau khi nghỉ, mỗi giai đoạn định tuyến chờ một lệnh đọc cache đã hết hạn, khoảng 250 ms, rồi giai đoạn sau lại như vậy. Phần chia giai đoạn cho thấy khoảng 250 ms mỗi giai đoạn ở gw-auth, gw-ctx và gw-prep, và 600 đến 940 ms ở gw-route.

Bản sửa là làm ấm các cache đó sớm. Ngay khi parse xong body của request, chúng tôi bắt đầu mọi lệnh đọc định tuyến không phụ thuộc người dùng, trong một đợt. Đợt đó chạy song song với bước kiểm tra key và rate limit. Các bước kiểm tra sau sẽ thấy giá trị đã nạp sẵn hoặc nhập vào lệnh đọc đang chạy.

Chúng tôi merge thay đổi này ngày 12 tháng 10 và merge bản revert 78 phút sau đó. Một số request chat không stream và API danh sách model bị treo 60 đến 120 giây.

Nguyên nhân là một quy tắc của Workers mà chúng tôi chưa tính tới. Khi một request trả về mà không đăng ký phần việc đang chờ bằng waitUntil, runtime sẽ huỷ phần việc đó, và một lệnh đọc storage bị huỷ thì không bao giờ kết thúc. Promise của nó không bao giờ resolve và cũng không reject. Một request lỗi sớm, ví dụ vì key sai, để lại các lệnh đọc làm ấm bị huỷ. Các lệnh đọc đó được chia sẻ với các request sau trong cùng isolate, nên mọi request sau đều chờ một promise không bao giờ xong.

Chúng tôi tái hiện lỗi trên runtime Workers chạy local rồi đưa thay đổi trở lại với hai bản sửa. Bước làm ấm giờ chạy dưới waitUntil, nên request khởi động nó sống tới khi nó xong. Một request nhập vào lệnh đọc của request khác giờ chờ tối đa 1,5 giây, sau đó bỏ slot dùng chung và tự đọc. Các test từng treo trên code cũ đều pass trên code mới.

Chúng tôi chưa đo lại đường sau khi nghỉ kể từ thay đổi đó, nên bài này không đưa ra con số cho nó.

Những trang không cần đánh thức Worker

Thay đổi thứ hai nằm ở website. Danh mục model, mọi trang model và datasheet Markdown của chúng không thay đổi theo người xem. Những phần động duy nhất, như độ trễ hiện tại hay key nhà cung cấp của chính bạn, được tải từ API sau khi trang hiện ra.

Vì vậy chúng tôi ngừng render các trang đó cho mỗi request. Một bước trong deploy dựng chúng thành file tĩnh, và Cloudflare phục vụ chúng từ edge mà không chạy Worker của chúng tôi. Trang docs làm điều tương tự ngay trong bước build của nó. Bước đó ghi ra 210 trang, và theo ước tính của chúng tôi chỉ thêm khoảng 10 giây cho một lần deploy docs.

Ngày 26 tháng 10, chúng tôi đo time to first byte bằng curl từ một máy. Với mỗi trang, chúng tôi gửi một request đầu tiên, rồi 12 request nối tiếp, và lấy trung vị của 12 request đó. Mốc so sánh là một endpoint của Cloudflare không chạy Worker nào, nên nó chỉ phản ánh mạng.

TrangTTFB trung vị khi ấm
Mốc mạng, không có Worker165 ms
Trang chủ docs, tĩnh198 ms
/models, tĩnh199 ms
Một trang model, tĩnh221 ms
Trang đăng nhập, Worker render ở mỗi request211 ms
Hai trang trên Worker khác, render ở mỗi request292 và 341 ms

Trang tĩnh không chạy code Worker nào, nên mỗi lượt xem không tốn thời gian Worker. Tuy vậy, đo từ một máy này, time to first byte của chúng gần với một trang do Worker render. Trang đăng nhập không có cache và render ở mỗi request, đo được 211 ms. Hai trang chậm hơn chạy trên Worker khác, và báo cáo này chưa tách được vì sao chúng chậm hơn. Vì vậy lợi ích chúng tôi chứng minh được từ trang tĩnh là phần việc của Worker cho mỗi lượt xem, không phải một mức giảm lớn của time to first byte.

Tiếp theo

Số đo trong lab và timing phía server không cho biết một trang có cảm giác thế nào trên điện thoại. Từ ngày 26 tháng 10, chúng tôi bắt đầu ghi timing từ người dùng thật. Đó là Core Web Vitals theo nhóm trang, và thời gian từ lúc gửi đến token đầu tiên trong playground. Chúng tôi giữ 20% phiên, rút gọn mỗi URL về nhóm trang của nó, và không ghi prompt, response hay key. Người dùng bật zero data retention thì không gửi gì cả. Chúng tôi cần đủ một tuần dữ liệu trước khi công bố số.

Các thay đổi sau đang được review và chưa ship. Số liệu của chúng đến từ test trong lab, không phải production.

  • Đọc cờ quarantine của upstream thành một bản ghi cho mỗi backend. Trên một isolate nguội, harness test đếm được 59 lệnh đọc storage cho một model có nhiều upstream trước thay đổi và 26 sau thay đổi.
  • Tách JavaScript của website để một trang không tải code của trang khác. Trên trang chủ, trong một bài test mobile có giới hạn tốc độ, thời gian bị chặn giảm từ 1.017 xuống 691 ms và JavaScript giảm từ 1.411 xuống 1.192 KB đã nén.
  • Phục vụ các trang use case dưới dạng file tĩnh, như các trang model.

Nếu một lần gọi qua AnyRouter có vẻ chậm, hãy xem header Server-Timing và gửi cho chúng tôi giá trị gw và upstream. Chúng cho chúng tôi biết ngay nên xem phía nào.

Gửi request đầu trong hai phút

Bắt đầu miễn phí với key của bạn, hoặc nạp rồi trả theo token. Go: $4 credit/tháng — $2/tháng, hoặc miễn phí khi góp một provider key.

Bắt đầu miễn phí