通过CloudFlare Worker搭建负载均衡服务器


Cloudflare Worker 是和 Amazon Lambda, Google Function 类似的无服务器 Serverless 技术. 我们可以写一些代码(JS)部署到 CloudFlare 的网络节点中. 这项技术的好处是我们并不需要去维护服务器(减少运维成本), 而且通过Serverless技术很容易就可以把程序跑在成千上万的节点上 (较强的可扩展性).

load-balancer 通过CloudFlare Worker搭建负载均衡服务器 CloudFlare 云计算 技术

load-balancer

负载均衡服务器(Load Balancer)用于把用户的请求重新分配(Route)到提供真正服务的源服务器(Worker). 我们可以通过负载均衡来实现水平扩展(Horizontal Scaling). 当然如果负载均衡只有一台服务器, 也是会有单点故障的 (Single Point of Failure).

cloudflare-edge-nodes-network 通过CloudFlare Worker搭建负载均衡服务器 CloudFlare 云计算 技术

cloudflare-edge-nodes-network

如果通过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?

GD Star Rating
loading...
本文一共 811 个汉字, 你数一下对不对.
通过CloudFlare Worker搭建负载均衡服务器. (AMP 移动加速版本)
上一篇: HPZ800服务器主板太老不支持超过2TB的大硬盘
下一篇: 在竞赛中通过向标准输出stdout打印数据来调试leetcode程序

扫描二维码,分享本文到微信朋友圈
e7d57ea0f3af0db5daa2fad6200ffdcd 通过CloudFlare Worker搭建负载均衡服务器 CloudFlare 云计算 技术

2 条评论

评论