Cloudflare worker, cache, KV storage. Những món thú vị dành cho Back-end developer.

Duy KN
9 min readFeb 24, 2021

Trước tiên xin khẳng định đây không phải là bài quảng bá dịch vụ Cloudflare. Mục đích của bài viết là note & share lại một số cái hay từ việc dùng Cloudflare nói chung và đặc biệt là Cloudflare Worker cho mục đích cá nhân. Bài viết không phải document chính thức, vui lòng tham khảo document chính thức của worker tại https://developers.cloudflare.com/workers/

Giới thiệu khái quát Cloudflare

Cloudflare có khá nhiều dịch vụ, trong phạm vi bài viết sẽ chỉ đề cập đến 2 dịch vụ quen thuộc là DNS và CDN.

Cơ chế kích hoạt CDN của Cloudflare khá đơn giản, tích hợp chung với việc khai báo DNS

Khi set Proxy status thành Proxied, Cloudflare ngoài việc làm DNS sẽ trở thành một “global” proxy aka. CDN cho subdomain tương ứng. Tất cả các request đi vào test.duynk.work sẽ đến hệ thống proxy của Cloudflare trước khi đến Origin server (trừ khi có custom config khác)

Từ màu sắc của icon proxy status, người ta hay dùng từ “Orange" để ám chỉ Proxy status là Proxied, và “Grey" cho DNS only

CDN của Cloudflare có đủ các chức năng từ cơ bản đến cao cấp, tùy vào nhu cầu bạn có thể tìm hiểu thêm. Các feature cơ bản có liên quan đến phần trình bài bên dưới là cache, firewall rule, và page rule (các rule & action khi có request và response)

Cloudflare và AnyCast

Cloudflare dùng 1 dãy IP chung cho tất cả các quốc gia họ triển khai hệ thống. Do đó khi dùng IP location thì không thể biết server (ở quốc gia/DC) nào của họ đang handle request.

Lúc nào tra cứu remote IP qua iplocation.net cũng là US cả

Dãy IP của Cloudflare https://www.cloudflare.com/ips/

Để routing được với cách này, họ dùng Anycast. Nhìn chung khá phức tạp và không thuộc phạm vi bài viết (thật ra mình cũng không hiểu hết để viết cho đúng), nên các bạn có thể xem thêm ở đây https://www.cloudflare.com/learning/cdn/glossary/anycast-network/

Cloudflare Worker

Cloudflare worker cơ bản là một webservice, nơi bạn có thể serve các http(s) request như trang HTML đơn giản hay API.

Bạn được cung cấp 1 entry point để lắng nghe http(s) request, xử lý logic, và trả kết quả.

Mình quen dùng NodeJS nên ngôn ngữ mình lựa chọn là Javascript. Các ngôn ngữ khác xem thêm ở đây https://developers.cloudflare.com/workers/platform/languages

Để đơn giản, mình dùng editor trên web sẵn có của Cloudflare

Cloudflare worker online editor

Sau khi deploy, bạn có thể gọi http(s) request đến domain https://sparkling-smoke-d758.duynkwork.workers.dev/

  • sparkling-smoke-d758: Tên worker, bạn có thể đặt tùy thích
  • duynkwork.workers.dev: subdomain mà Cloudflare worker cấp cho bạn

Như vậy không cần có thuê server, setup, …, bạn đã có 1 web service cho một số công việc cơ bản.

Sau khi Save and Deploy, có thể test trực tiếp trên giao diện. Debug bằng console.log trong code (với javascript)

Worker giới hạn sử dụng bởi các API được cung cấp sẳn. Không kỳ vọng worker sẽ hoạt động như một web server framework có thể include các 3rd party lib như Node JS , PHP, …

Có khá nhiều ví dụ thú vị bạn có thể thử https://developers.cloudflare.com/workers/tutorials

Add route cho worker

Như bạn thấy thì domain dùng để call API trên là duynkwork.workers.dev , không phải domain của bạn, và nó có 1 số vấn đề.

Theo như mình thấy (thông tin cần kiểm chứng), Cloudflare Worker là một dịch vụ không “nằm trong” pipeline của Cloudflare CDN. Mình nhấn mạnh ý này vì sẽ có 1 số issue phát sinh như bên dưới:

  • Các request đến worker thông qua link này thường đi đến một server rất xa xôi, không được routing đến edge tốt nhất như cơ chế CDN thông thường của Cloudflare.
  • Không sử dụng được các feature của CDN như Firewall Rule và Page rule
  • Vấn đề worker gọi worker (trình bày bên dưới)

Để kết hợp worker vào CDN cần add route cho worker.

Để add route cho worker, trước tiên cần tạo 1 orange subdomain dạng AAAA trỏ vào preserve IP 100::

Chổ này chủ yếu là bạn tạo ra 1 orange DNS record, có thể là A, CNAME tùy ý mà không ảnh hưởng đến cấu hình chung. Chọn AAAA với IP 100:: cho tiện.

Vào phần quản lý worker, chọn add route. Route có thể là subdomain, hoặc cũng có thể là 1 path cụ thể.

Add route cho worker

Subrequest — Fetch — Proxy

Worker có thể sử dụng cho nhiều mục đích, có thể là tính toán, hoặc tự động hóa một việc gì đó.

Thông thường mình dùng cho việc “proxy”. Với lợi thế hạ tầng mạnh, chịu được request lớn, cache tốt, thì việc dụng Worker làm proxy chịu tải khá tốt.

Worker cung cấp fetch API để từ worker có thể fetch kết quả từ một URL nào đó. Cách dùng tương đối đơn giản. https://developers.cloudflare.com/workers/runtime-apis/fetch

Một điểm lưu ý với fetch là URL không nên có custom port. Thực tế thử nghiệm cho thấy không thể call về url với custom port. (Hoặc có thể chưa biết cách sử dụng)

addEventListener('fetch', event => {
var url = new URL(event.request.url);

// https://example.com/path/ to https://myorigin.example.com/path
url.hostname = 'myorigin.' + url.hostname

event.respondWith(fetch(url));
});

Ngoài ra có thể proxy cho mục đích share tải giữa các origin. (Chỉ mang tính tận dụng. Để có thể dùng như load balancing thật thụ thì Cloudflare có một service chuyên dụng Cloudflare Load Balancing)

var hostnames = [
"0.example.com",
"1.example.com",
"2.example.com"
];

addEventListener('fetch', event => {
var url = new URL(event.request.url);

// Randomly pick the next host
url.hostname = hostnames[getRandomInt(hostnames.length)];

event.respondWith(fetch(url));
});

function getRandomInt(max) {
return Math.floor(Math.random() * max);
}

https://blog.cloudflare.com/update-response-headers-on-cloudflare-workers/

Await vs. WaitUntil

Với các API dạng Promise như fetch, trong trường hợp không cần chờ kết quả trả về có thể sử dụng event.waitUntil

  • Worker giới hạn thời gian xử lý, event.waitUntil có thể sử dụng để xử lý các request tốn thời gian. Việc không phải chờ kết quả trả về, giúp giảm thời gian chờ thực thi https://developers.cloudflare.com/workers/platform/limits#cpu-runtime
  • Tuy nhiên, do không thể biết kết quả trả về và lỗi trả về (nếu có) lúc runtime, cần thận trọng.
  • Thường sử dụng cho các mục đích như 3rd party log, cache, stream.
await fetch(url)event.waitUntil(fetch(url))

Vấn đề caching của subrequest.

Theo ghi nhận, repsonse từ worker không được cache. Khi user request, request luôn đến worker. (Cần kiểm chứng lại thông tin)

Do đó nên chủ động cache các sub-request. Việc cache các subrequest tương đối dễ rối cho người mới do có đến 3 cơ chế caching (nếu không tính đến KV storage):

  • Cache dựa trên page rules
  • Cache bằng fetch API với RequestInitCfProperties
  • Cache bằng cache API

Note: Cache của Cloudflare là độc lập giữa các colo (cụm server của Cloudflare), giải thích bên dưới.

Với Cache dựa trên page rules: thường thì mình không dùng, và thấy cũng không có tác dụng với worker. (Cần kiểm chứng lại thông tin)

Với cách bằng fetch API + RequestInitCfProperties. Mục đích chính là cache lại các subrequest, mà không quan tâm đến chuyện update cache, purge cache. Ví dụ worker dùng làm proxy, fetch xuống origin và có nhu cầu cache lại đơn giản, thì có thể dùng API sau.

fetch(event.request, {
cf: {
cacheKey: ".......",
cacheTtlByStatus: {
"200-299": 86400,
404: 1,
"500-599": 0
}
}
})

(https://developers.cloudflare.com/workers/runtime-apis/request#parameters)

Cache bằng cache API:

Cách này cho phép developer tùy biến linh hoạt cách sử dụng. Có thể chủ động cache, xóa cache.

async function fetchOrigin(event) {
let cache = caches.default
cacheKey = doSomethingWithInputURL(event.request.url)

const respone = await cache.match(cacheKey)
if (!respone) {
respone = fetch(...)
if (resp.status >= 200 & resp.status <= 299)
response.headers.append("Cache-Control", "max-age=120")
event.waitUntil(cache.put(cacheKey, respone.clone()))
}
}
return respone
}

Lưu ý:

  • cacheKey trong trường hợp này phải là url format hoặc object kiểu Request
  • Cache API không work trong online editor của Worker.
  • Có thể linh động custom lại cacheKey dựa trên request ban đầu
  • cacheKey này không liên quan đến cf.cacheKey trong Resquest interface ở cách trên.
  • Hàm cache.put không có các params cho việc quy định cấu hình cache. Do đó, cần custom header Cache-Control của response như trên, trong trường hợp này là TTL (max-age)
  • Với kết quả tính toán, có thể tận dụng cache API (với 1 mock URL nào đó tùy ý), hoặc dùng KV storage như cách bên dưới.

Caching & Colo

Một điểm khá đặc biệt của Cloudflare là việc caching là hoàn toàn độc lập giữa các colo. Ví dụ một request có thể được cache ở cụm SGN, thì không có nghĩa nó cũng được cache ở HAN. Cloudflare không tự sync cache giữa các colo, cũng như không có cơ chế precache hay warm-up

Việc này tương đối bất tiện, ví dụ như ở VN, request có thể rơi vào 3 cụm SGN (TPHCM), HAN (Hà Nội), hoặc SIN(Singapore) tùy theo nhà mạng. Với tình trạng cáp quang hay đứt ở VN, nếu user không may rơi vào cụm SIN khi chưa cache thì rất bất tiện.

Có thể quan sát header 3 ký tự cuối CF-RAY Header để biết cụm nào đã xử lý request

CF-RAY: 626a77681c62d05c-SGN

Hoặc code thì có thể dùng field colo của IncomingRequestCfProperties để biết (https://developers.cloudflare.com/workers/runtime-apis/request)

Status các cụm https://www.cloudflarestatus.com/

KV Storage

KV Storage là một distributed key-value database, vừa đủ dùng.

Vừa đủ dùng là vì KV Storage chỉ cung cấp các chức năng cơ bản như: get, put, list, delete.

Khác với các cơ chế cache ở trên độc lập từng colo. KV Storage sync tất cả các colo.

Điều cần lưu ý với KV Storage là chi phí sử dụng (chi phí gọi API và storage).

Để sử dụng KV Storage:

  • Đầu tiên, cần tạo một namespace . Có thể hiểu là tên database.
  • Bind namespace vào worker
Tạo KV Storage namespace và bind vào worker

Trong worker, lúc này có thể dùng SAMPLE_DB_VAR để truy xuất Storage

const data = await SAMPLE_DB_VAR.get(key)
if (!data) {
const value = doSomeThing()
data await SAMPLE_DB_VAR.put(key, value, {expirationTtl: 120})
}
return data

Hạn chế của KV Storage

Nếu bạn kỳ vọng KV Storage có thể hoạt động tương tự Redis thì có thể bạn thất vọng. Đơn cử là KV Storage không thể support xử lý phép toán INCR. Ví dụ bạn cần 1 counter để đếm số lần request đã thực hiện:

const oldValue = parseInt(await SAMPLE_DB_VAR.get(key))
SAMPLE_DB_VAR.put(key, oldValue + 1)

Do xử lý song song và không đảm bảo consistent, bạn không thể đảm bảo có kết quả đúng.

Trong trường hợp này, Durable Object là giải pháp thích hợp.

Worker và Page Rule

Sau khi add route vào Orange DNS, Worker sẽ thừa kế các feature của CloudFlare CDN như Firewall Rule và Page Rule. Tuy nhiên không phải page rule nào cũng có tác dụng hoặc ít nhất là “thấy” có tác dụng. Tùy theo rule:

  • Có có thể có tác dụng lên request trước khi đến worker;
  • hoặc sau lên response trả về từ worker;
  • hoặc tác dụng lên subrequest trong worker

Trừ 1 số trường hợp rất đặc biệt, thông thường hầu như mình không cần dùng PageRule chung với worker.

Worker fetches worker — Error: 1000

Một worker sẽ có thể fetch 1 worker API khác nếu như cả 2 domain đó không phải cùng là Orange.

Ví dụ, hệ thống sẽ trả lỗi nếu bạn gọi worker bằng orange subdomain test.duynk.work, trong khi worker đó lại fetch 1 worker khác cùng domain chính, ví dụtest2.duynk.work cũng là orange, sẽ phát sinh lỗi Error 1000 — DNS points to prohibited IP

Trong trường hợp này, một trong 2 worker phải dùng domain workers.dev

Error 523: Origin is unreachable

Thi thoảng gặp vấn đề này với fetch, chỉ xãy ra với domain orange đã add route, không xãy ra trên môi trường dev.

Tìm xung quanh thì cũng có 1 số thông tin như: https://community.cloudflare.com/t/community-tip-fixing-error-523-origin-is-unreachable/44218.

Một cách work arround đã work là chuyển từ SSL từ flexible sang full.

Your connection is not private

Sau khi workaround bằng cách Disable Universal SSL thì có thể chạy bình thường. Khả năng trình duyệt không chấp nhận Universal SSL của Cloudflare vì lý do nào đó. Khuyến cáo nên mua Advanced SSL để tránh những phiền phức liên quan đến SSL.

Các vấn đề khác

Worker còn các feature khá thú vị là:

Bài viết chỉ tóm tắt một số điểm gặp phải trong quá trình sử dụng. Tất cả có trong https://developers.cloudflare.com/workers/

--

--