2

37. 干货系列从零用Rust编写负载均衡及代理,负载均衡中try_files实现 - 问蒙服务框架

 4 months ago
source link: https://www.cnblogs.com/wmproxy/p/wmproxy37.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

wmproxy

wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子

国内: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

nginx中的try_files

  • 语法:try_files file … uri;try_files file … = code;
  • 作用域:server location
    • 首先:按照指定的顺序检查文件是否存在,并使用第一个找到的文件进行请求处理
    • 其次:处理是在当前上下文中执行的。根据 root 和 alias 指令从 file 参数构造文件路径。
    • 然后:可以通过在名称末尾指定一个斜杠来检查目录的存在,例如"$uri/"
    • 最后:如果没有找到任何文件,则进行内部重定向到最后一个参数中指定的 uri。

注:只有最后一个参数可以引起一个内部重定向,之前的参数只设置内部的 URL 的指向。最后一个参数是回退 URL 且必须存在,否则会出现内部 500 错误。命名的 location 也可以使用在最后一个参数中。

1、前端路由处理:

location / { try_files $uri $uri/ /index.html; # $uri指请求的uri路径,$uri/表示请求的uri路径加上一个/,例如访问example.com/path,则会依次尝试访问/path,/path/index.html,/index.html # /index.html表示如果仍未匹配到则重定向到index.html}

这种场景多用于单页应用,例如vue.js等前端框架的路由管理。当用户在浏览器中访问一个非根路径的路径时,由于这些路径都是由前端路由管理的,nginx无法直接返回正确的静态文件,因此需要将请求重定向到统一的路径,这里是/index.html,由前端路由控制页面的展示。
2、图片服务器:

location /images/ { root /data/www; error_page 404 = /fetch_image.php; try_files $uri $uri/ =404;}location /fetch_image.php { fastcgi_pass 127.0.0.1:9000; set $path_info ""; fastcgi_param PATH_INFO $path_info; fastcgi_param SCRIPT_FILENAME /scripts/fetch_image.php; include fastcgi_params;}

这种场景多用于图片服务器,当用户访问图片时,先尝试在本地文件系统中查找是否有该文件,如果找到就返回;如果没有找到则会转发到fetch_image.php进行处理,从远程资源服务器拉取图片并返回给用户。

当前nginx方案的实现,是基于文件的重试,也就是所谓的伪静态,如果跨目录的服务器就很麻烦了,比如:

location /images/ { root /data/upload; try_files $uri $uri/ =404;}location /images2/ { root /data/www; try_files $uri $uri/ =404;}

上面的我们无法同时索引两个目录下的结构。即我假设我访问/images/logo.png无法同时查找/data/upload/logo.png/data/www/logo.png是否存在。

当前实现方案从try_files变成try_paths也就是当碰到该选项时,将当前的几个访问地址重新进入路由

[[http.server.location]]rate_limit = "4m/s"rule = "/root/logo.png"file_server = { browse = true }proxy_pass = ""try_paths = "/data/upload/logo.png /data/www/logo.png /root/README.md" [[http.server.location]]rule = "/data/upload"file_server = { browse = true } [[http.server.location]]rule = "/data/www"file_server = { browse = true }

除非碰到返回100或者200状态码的,否则将执行到最后一个匹配路由。

    1. 要能循环遍历路由
    1. 当try_paths时要避免递归死循环
    1. 当try_paths时可能会调用自己本身,需要能重复调用

以下主要源码均在reverse/http.rs

  • 实现循环
    主要的处理函数为deal_match_location,函数的参数为
#[async_recursion]async fn deal_match_location( req: &mut Request<Body>, // 缓存客户端请求 cache: &mut HashMap< LocationConfig, (Sender<Request<Body>>, Receiver<ProtResult<Response<Body>>>), >, // 该Server的配置选项 server: Arc<ServerConfig>, // 已处理的匹配路由 deals: &mut HashSet<usize>, // 已处理的TryPath匹配路由 try_deals: &mut HashSet<usize>,) -> ProtResult<Response<Body>>

当前在Rust中的异步递归会报如下错误

recursion in an `async fn` requires boxinga recursive `async fn` must be rewritten to return a boxed `dyn Future`consider using the `async_recursion` crate: https://crates.io/crates/async_recursion

所以需要添加#[async_recursion]或者改成Box返回。

参数其中多定义了两组HashSet用来存储已处理的路由及已处理的TryPath路由。

将循环获取合适的location,如果未找到直接返回503错误。

let path = req.path().clone();let mut l = None;let mut now = usize::MAX;for idx in 0..server.location.len() { if deals.contains(&idx) { continue; } if server.location[idx].is_match_rule(&path, req.method()) { l = Some(&server.location[idx]); now = idx; break; }}if l.is_none() { return Ok(Response::status503() .body("unknow location to deal") .unwrap() .into_type());}

当该路由存在try_paths的情况时:

// 判定该try是否处理过, 防止死循环if !try_deals.contains(&now) && l.try_paths.is_some() { let try_paths = l.try_paths.as_ref().unwrap(); try_deals.insert(now); let ori_path = req.path().clone(); for val in try_paths.list.iter() { deals.clear(); // 重写path好方便做数据格式化 req.set_path(ori_path.clone()); let new_path = Helper::format_req(req, &**val); // 重写path好方便后续处理无感 req.set_path(new_path); if let Ok(res) = Self::deal_match_location( req, cache, server.clone(), deals, try_deals, ) .await { if !res.status().is_client_error() && !res.status().is_server_error() { return Ok(res); } } } return Ok(Response::builder() .status(try_paths.fail_status) .body("not valid to try") .unwrap() .into_type());}

其中会将req中的path进行格式化的重写以方便处理:

// 重写path好方便做数据格式化req.set_path(ori_path.clone());let new_path = Helper::format_req(req, &**val);// 重写path好方便后续处理无感req.set_path(new_path);

如果不存在try_paths将正常的按照路由的处理逻辑,该文件服务器或者反向代理,并标记该路由已处理。

deals.insert(now);let clone = l.clone_only_hash();if cache.contains_key(&clone) { let mut cache_client = cache.remove(&clone).unwrap(); if !cache_client.0.is_closed() { println!("do req data by cache"); let _send = cache_client.0.send(req.replace_clone(Body::empty())).await; match cache_client.1.recv().await { Some(res) => { if res.is_ok() { log::trace!("cache client receive response"); cache.insert(clone, cache_client); } return res; } None => { log::trace!("cache client close response"); return Ok(Response::status503() .body("already lose connection") .unwrap() .into_type()); } } }} else { log::trace!("do req data by new"); let (res, sender, receiver) = l.deal_request(req).await?; if sender.is_some() && receiver.is_some() { cache.insert(clone, (sender.unwrap(), receiver.unwrap())); } return Ok(res);}

try_files在nginx中提供了更多的可能,也方便了伪静态文件服务器的处理。我们在其中的基础上稍微改造成try_paths来适应处理提供多路由映射的可能性。

2631821-20231227090003627-1122789971.png

点击 [关注][在看][点赞] 是对作者最大的支持


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK