Cloudflare Worker 是和 Amazon Lambda, Google Function 类似的无服务器 Serverless 技术. 我们可以写一些代码(JS)部署到 CloudFlare 的网络节点中. 这项技术的好处是我们并不需要去维护服务器(减少运维成本), 而且通过Serverless技术很容易就可以把程序跑在成千上万的节点上 (较强的可扩展性).
负载均衡服务器(Load Balancer)用于把用户的请求重新分配(Route)到提供真正服务的源服务器(Worker). 我们可以通过负载均衡来实现水平扩展(Horizontal Scaling). 当然如果负载均衡只有一台服务器, 也是会有单点故障的 (Single Point of Failure).
如果通过CloudFlare Worker来搭建负载均衡, 这样我们的负载均衡服务器会被自动部署到成千上万的CloudFlare节点中 – 可靠性 Durability 和可用性 Availability 就很靠谱了.
通过CloudFlare Worker搭建的分布式负载均衡服务器
设置分布式负载平衡器的成本是可以承受的. CloudFlare工作者有一个免费计划-每天为您提供10万个API调用, 每个API请求的最大CPU时间为10毫秒. 对于付费计划-每月报价为1000万个请求, 最长50ms CPU时间.
例如, 让我们首先定义分布式负载均衡器后面的源服务器列表. 源服务器也就是真正干活的节点.
1 2 3 4 | let nodes = [ "https://api.justyy.com", "https://api.steemyy.com" ]; |
let nodes = [ "https://api.justyy.com", "https://api.steemyy.com" ];
我们需要一个 Promise.any 实现方法 – 在 CloudFlare Worker 里 并不支持 Promise.any 但我们可以定义如下:
1 2 3 4 5 6 7 | function reverse(promise) { return new Promise((resolve, reject) => Promise.resolve(promise).then(reject, resolve)); } function promiseAny(iterable) { return reverse(Promise.all([...iterable].map(reverse))); }; |
function reverse(promise) { return new Promise((resolve, reject) => Promise.resolve(promise).then(reject, resolve)); } function promiseAny(iterable) { return reverse(Promise.all([...iterable].map(reverse))); };
Promise.race 是返回第一个被实现或者拒绝的 Promise 而相反 Promise.any 是返回第一个被实现的 Promise. 所以用在这里就是获得响应最快(成功)的源服务器.
1 2 3 4 5 6 7 8 9 10 11 12 13 | async function contactServer(server) { return new Promise((resolve, reject) => { fetch(server, { method: "GET" }).then(response => { resolve({ "server": server, }); }).catch(function(error) { reject(error); }); }); } |
async function contactServer(server) { return new Promise((resolve, reject) => { fetch(server, { method: "GET" }).then(response => { resolve({ "server": server, }); }).catch(function(error) { reject(error); }); }); }
我们还可以加于改进, 为每个源服务器添加一个返回当前负载的API – 这样一来, 我们就可以选出负载最小的那个源服务器了.
处理 跨域 CORs 和头部数据 Headers
以下JS代码用于处理跨域和相应的Headers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | function handleOptions(request) { // Make sure the necesssary headers are present // for this to be a valid pre-flight request if ( request.headers.get('Origin') !== null && request.headers.get('Access-Control-Request-Method') !== null && request.headers.get('Access-Control-Request-Headers') !== null ) { // Handle CORS pre-flight request. // If you want to check the requested method + headers // you can do that here. return new Response(null, { headers: corsHeaders, }) } else { // Handle standard OPTIONS request. // If you want to allow other HTTP Methods, you can do that here. return new Response(null, { headers: { Allow: 'GET, HEAD, POST, OPTIONS', }, }) } } addEventListener('fetch', event => { const request = event.request; const method = request.method.toUpperCase(); if (method === 'OPTIONS') { // Handle CORS preflight requests event.respondWith(handleOptions(request)) } else if ( method === 'GET' || method === 'HEAD' || method === 'POST' ) { // Handle requests to the API server event.respondWith(handleRequest(request)) } else { event.respondWith( new Response(null, { status: 405, statusText: 'Method Not Allowed', }), ) } }); |
function handleOptions(request) { // Make sure the necesssary headers are present // for this to be a valid pre-flight request if ( request.headers.get('Origin') !== null && request.headers.get('Access-Control-Request-Method') !== null && request.headers.get('Access-Control-Request-Headers') !== null ) { // Handle CORS pre-flight request. // If you want to check the requested method + headers // you can do that here. return new Response(null, { headers: corsHeaders, }) } else { // Handle standard OPTIONS request. // If you want to allow other HTTP Methods, you can do that here. return new Response(null, { headers: { Allow: 'GET, HEAD, POST, OPTIONS', }, }) } } addEventListener('fetch', event => { const request = event.request; const method = request.method.toUpperCase(); if (method === 'OPTIONS') { // Handle CORS preflight requests event.respondWith(handleOptions(request)) } else if ( method === 'GET' || method === 'HEAD' || method === 'POST' ) { // Handle requests to the API server event.respondWith(handleRequest(request)) } else { event.respondWith( new Response(null, { status: 405, statusText: 'Method Not Allowed', }), ) } });
转发请求
负载均衡最重要的部分就是转发请求. 一旦我们知道哪个(最快)服务器应满足当前请求. 然后, 我们需要将请求转发到原始服务器, 并在获得结果后将其转发回用户. 以下是分别转发GET和POST请求的两个功能-您可能希望添加其他请求, 例如PUT, PATCH, DELETE等.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | async function forwardRequestGET(apiURL) { return new Promise((resolve, reject) => { fetch(apiURL, { method: "GET", headers: { 'Content-Type': 'application/json' }, redirect: "follow" }).then(response => { resolve(response.text()); }).catch(function(error) { reject(error); }); }); } async function forwardRequestPOST(apiURL, body) { return new Promise((resolve, reject) => { fetch(apiURL, { method: "POST", redirect: "follow", headers: { 'Content-Type': 'application/json' }, body: body }).then(response => { resolve(response.text()); }).catch(function(error) { reject(error); }); }); } |
async function forwardRequestGET(apiURL) { return new Promise((resolve, reject) => { fetch(apiURL, { method: "GET", headers: { 'Content-Type': 'application/json' }, redirect: "follow" }).then(response => { resolve(response.text()); }).catch(function(error) { reject(error); }); }); } async function forwardRequestPOST(apiURL, body) { return new Promise((resolve, reject) => { fetch(apiURL, { method: "POST", redirect: "follow", headers: { 'Content-Type': 'application/json' }, body: body }).then(response => { resolve(response.text()); }).catch(function(error) { reject(error); }); }); }
使用CloudFlare Worker的负载均衡器服务器
以下是CloudFlare负载均衡服务器的主要实现部分. 该脚本将在CloudFlare分布缘网络节点上运行.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | /** * Respond to the request * @param {Request} request */ async function handleRequest(request) { const country = request.headers.get('cf-ipcountry'); const servers = []; for (const server of nodes) { servers.push(contactServer(server)); } const load = await promiseAny(servers); const forwardedURL = load['server']; const method = request.method.toUpperCase(); let result; let res; let version = ""; try { version = await getVersion(load['server']); } catch (e) { version = JSON.stringify(e); } try { if (method === "POST") { const body = await request.text(); result = await forwardRequestPOST(forwardedURL, body); } else if (method === "GET") { result = await forwardRequestGET(forwardedURL); } else { res = new Response(null, { status: 405, statusText: 'Method Not Allowed', }); res.headers.set('Access-Control-Allow-Origin', '*'); res.headers.set('Cache-Control', 'max-age=3600'); res.headers.set("Origin", load['server']); res.headers.set("Country", country); return res; } res = new Response(result, {status: 200}); res.headers.set('Content-Type', 'application/json'); res.headers.set('Access-Control-Allow-Origin', '*'); res.headers.set('Cache-Control', 'max-age=3'); res.headers.set("Origin", load['server']); res.headers.set("Version", version); res.headers.set("Country", country); } catch (e) { res = new Response(JSON.stringify(result), {status: 500}); res.headers.set('Content-Type', 'application/json'); res.headers.set('Access-Control-Allow-Origin', '*'); res.headers.set('Cache-Control', 'max-age=3'); res.headers.set("Origin", load['server']); res.headers.set("Version", version); res.headers.set("Country", country); res.headers.set("Error", JSON.stringify(e)); } return res; } |
/** * Respond to the request * @param {Request} request */ async function handleRequest(request) { const country = request.headers.get('cf-ipcountry'); const servers = []; for (const server of nodes) { servers.push(contactServer(server)); } const load = await promiseAny(servers); const forwardedURL = load['server']; const method = request.method.toUpperCase(); let result; let res; let version = ""; try { version = await getVersion(load['server']); } catch (e) { version = JSON.stringify(e); } try { if (method === "POST") { const body = await request.text(); result = await forwardRequestPOST(forwardedURL, body); } else if (method === "GET") { result = await forwardRequestGET(forwardedURL); } else { res = new Response(null, { status: 405, statusText: 'Method Not Allowed', }); res.headers.set('Access-Control-Allow-Origin', '*'); res.headers.set('Cache-Control', 'max-age=3600'); res.headers.set("Origin", load['server']); res.headers.set("Country", country); return res; } res = new Response(result, {status: 200}); res.headers.set('Content-Type', 'application/json'); res.headers.set('Access-Control-Allow-Origin', '*'); res.headers.set('Cache-Control', 'max-age=3'); res.headers.set("Origin", load['server']); res.headers.set("Version", version); res.headers.set("Country", country); } catch (e) { res = new Response(JSON.stringify(result), {status: 500}); res.headers.set('Content-Type', 'application/json'); res.headers.set('Access-Control-Allow-Origin', '*'); res.headers.set('Cache-Control', 'max-age=3'); res.headers.set("Origin", load['server']); res.headers.set("Version", version); res.headers.set("Country", country); res.headers.set("Error", JSON.stringify(e)); } return res; }
我们还可以在负载均衡服务器返回数据前添加一些Headers数据, 比如我们可以 获得源服务器版本 信息.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | async function getVersion(server) { return new Promise((resolve, reject) => { fetch(server, { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({"id":0,"jsonrpc":"2.0","method":"call","params":["login_api","get_version",[]]}) }).then(response => { resolve(response.text()); }).catch(function(error) { reject(error); }); }); } |
async function getVersion(server) { return new Promise((resolve, reject) => { fetch(server, { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({"id":0,"jsonrpc":"2.0","method":"call","params":["login_api","get_version",[]]}) }).then(response => { resolve(response.text()); }).catch(function(error) { reject(error); }); }); }
通过实现这种分布式负载均衡器 (Distributed Load Balancer) 节点, 我们以低成本提高了可用性 avaibability 和耐用性 durability , 因为由于无服务器技术 serverless , 我们实际上不需要维护(监控升级和安全补丁)服务器. 同时, 我们正在通过CloudFlare工作程序将请求转发到”最快的”原始服务器, 该节点在地理位置上会接近用户(拥有低延迟).
我们也可以: 通过AWS Lambda / API Gateway 架设负载均衡API服务器 (Load Balancer)
英文: Tutorial: How to Set Up a API Load Balancer by Using CloudFlare Worker?
loading...
上一篇: HPZ800服务器主板太老不支持超过2TB的大硬盘
下一篇: 在竞赛中通过向标准输出stdout打印数据来调试leetcode程序
话说你这个是并发请求 接受最早返回的 不算负载均衡吧
负载均衡有很多算法, 可以是 Round-robin, 可以是 least load, 为什么就不能是 fastest 呢? 返回最快的那个而且必须是200的.