Pingora快速入门教程1之总览

文章目录

Pingora一直是我比较期待的项目,所以写个入门系列教程吧。之前研究过一些其他产品,比如nginx, nginx-ingress, apisix, bfe等, 前三者植入业务基本靠lua或者说openrestybfe的社区实在是一言难尽, 所以我认为Pingora会成为Rust的明星项目,虽然没达到预期,但至少提供了一个框架,一个社区。

关于Pingora的介绍就翻译两句话吧,它是一个用于构建快速,可信赖,可编程网络系统的rust框架。它已经经过充分的测试,因为最近几年来,它每秒处理的网络请求已超过4000万次。

之所以没有达到预期,是因为在Pingora开源之前,大多人都认为会是类似于Nginx这样开箱即用的服务,但是Pingora只提供了一个框架,具体的逻辑要自己写,不能通过简单的配置就直接使用,这实在是会劝退一堆人,再者Rust还有一定的学习门槛。不过我觉得,Pingora应该会不断演进或者由第三方牵头演变出一套成熟的可复用的配置语法以及发展出一个不错的社区。

我简单使用和看了一下Pingora的源代码,感觉还是很清晰明了的,本文主要是关于Pingora系列文章的总览以及一个简单的入门教程。

以后有机会再出源码阅读的文章

系列文章规划如下:

  • 总览: 关于Pingora的简单使用及概念简单介绍
  • 钩子函数: Pingora提供的各个钩子函数, 通过它可以在Pingora转发流量前后处理各种逻辑
  • 灰度发布/负载均衡: 基于Cookie, Path, Query, Header等参数执行不同的转发逻辑
  • 服务管理: 管理应用的启停升级等功能
  • 测试: 写代码肯定要写测试的啦。
  • 深入理解?: 待定

Cargo.toml依赖如下

[dependencies]
async-trait = "0.1.77"
#env_logger = "0.11.3"
log = "0.4.21"
pingora-core = "0.1.0"
pingora-http = "0.1.0"
pingora-load-balancing = "0.1.0"
pingora-proxy = "0.1.0"
structopt = "0.3.26"
#openssl-sys = "0.9.39"
env_logger = "0.9"

运行环境: Centos7

rust版本: rustc 1.70.0 (90c541806 2023-05-31)

环境准备

首先我们需要搞两个后端,可以使用以下命令创建对应的命令

当然了,你可以按照自己的喜欢使用自己熟悉的工具来创建后端服务,比如使用axum现写几个后端。

mkdir web1 web2
echo web1 > web1/index.html
echo web2 > web2/index.html

然后打开两个终端, 分别进入刚好创建的目录中,即web1web2中, 执行以下命令。

# 在web1目录执行以下命令
python3 -m http.server 10081

# 在web2目录执行以下命令
python3 -m http.server 10082

这样就简单的拥有了两个简单的后端,分别返回web1web2, 可以通过curl命令测试,比如。

$ curl 127.0.0.1:10081
# 返回如下
web1

其实也可以通过在代码中创建一个后台服务来创建测试的后端,但是代码会显得不够简单明了,所以选择了上面这种稍微不那么一键式的方式。

快速入门

下面的代码主要改自官方示例,然后去除了TLS相关的内容。

// https://youerning.top
use async_trait::async_trait;
use log::info;
use pingora_core::services::background::background_service;
use std::{sync::Arc, time::Duration};
use structopt::StructOpt;

use pingora_core::server::configuration::Opt;
use pingora_core::server::Server;
use pingora_core::upstreams::peer::HttpPeer;
use pingora_core::{Result, Error, ErrorType};
use pingora_load_balancing::{health_check, selection::RoundRobin, LoadBalancer};
use pingora_proxy::{ProxyHttp, Session};

// 随便定义一个可以实现trait的struct对象
// 为了简单起见当然是要包含Pingora提供的负载均衡对象啦.
pub struct LB(Arc<LoadBalancer<RoundRobin>>);


#[async_trait]
impl ProxyHttp for LB {
    type CTX = ();
    fn new_ctx(&self) -> Self::CTX {}

    // ProxyHttp唯一必须要自己定义的方法, 也就是每次请求来会调用这个方法以选择后端(upstream)
    async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut ()) -> Result<Box<HttpPeer>> {
        let upstream = match self
            .0
            // 这里使用的负载均衡算法是RoundRobin, 所以key不重要, key是用用于hash算法的
            // 后面做灰度发布会用到hash算法
            .select(b"", 256) { 
                Some(upstream) => upstream,
                None => {
                    // 因为本代码中创建了一个健康检查的服务,所以可能会出现没有后端的情况
                    return Err(Error::new(ErrorType::new("没有健康的后端可以选择.")))
                }
            };
            

        info!("选择的后端是: {:?}", upstream);
        let peer = Box::new(HttpPeer::new(upstream, false, "".to_string()));
        Ok(peer)
    }

    // Pingora提供了很多的钩子函数,这个钩子函数用于修改或者说过滤客户端请求
    async fn upstream_request_filter(
        &self,
        _session: &mut Session,
        upstream_request: &mut pingora_http::RequestHeader,
        _ctx: &mut Self::CTX,
    ) -> Result<()> {
        // 将发送给后端的请求插入一个Host请求头
        upstream_request
            .insert_header("Host", "youering.top")
            .unwrap();
        Ok(())
    }
}

fn main() {
    // 初始化日志服务,默认级别是Error及以上才会显示, 可以通过RUST_LOG=INFO来调整日志输出级别
    env_logger::init();

    // 读取命令行参数
    let opt = Opt::from_args();
    let mut my_server = Server::new(Some(opt)).unwrap();
    my_server.bootstrap();

    // 127.0.0.1:343 是一个不存在的服务, 这样就会在Pingora的输出看到一个错误,说127.0.0.1:343不健康
    // 不健康的服务就不会被选择了
    let mut upstreams =
        LoadBalancer::try_from_iter(["127.0.0.1:10081", "127.0.0.1:10082", "127.0.0.1:343"]).unwrap();

    // 这里创建了一个健康检查的服务,健康检查服务会保证不健康的服务不会被选择
    let hc = health_check::TcpHealthCheck::new();
    upstreams.set_health_check(hc);
    upstreams.health_check_frequency = Some(Duration::from_secs(1));

    // 创建一个后台服务
    let background = background_service("健康检查", upstreams);
    let upstreams = background.task();

    // 创建一个代理服务
    let mut lb = pingora_proxy::http_proxy_service(&my_server.configuration, LB(upstreams));
    lb.add_tcp("0.0.0.0:10080");

    my_server.add_service(lb);
    my_server.add_service(background);
    my_server.run_forever();
}

然后通过以下命令启动:

RUST_LOG=INFO cargo run

如果启动完毕,会发现马上报错,比如下面这样。

[2024-03-13T08:42:08Z INFO  pingora_core::server] Bootstrap starting
[2024-03-13T08:42:08Z INFO  pingora_core::server] Bootstrap done
[2024-03-13T08:42:08Z INFO  pingora_core::server] Server starting
[2024-03-13T08:42:08Z WARN  pingora_load_balancing] Backend { addr: Inet(127.0.0.1:343), weight: 1 } becomes unhealthy,  ConnectRefused context: Fail to connect to BasicPeer { _address: Inet(127.0.0.1:343), sni: "", options: PeerOptions { bind_to: None, connection_timeout: Some(1s), total_connection_timeout: None, read_timeout: None, idle_timeout: None, write_timeout: None, verify_cert: true, verify_hostname: true, alternative_cn: None, alpn: H1, ca: None, tcp_keepalive: None, no_header_eos: false, h2_ping_interval: None, max_h2_streams: 1, extra_proxy_headers: {}, curves: None, second_keyshare: true, tracer: None } } cause:  context: Fail to connect to 127.0.0.1:343 cause: Connection refused (os error 111)

上面的错误提示127.0.0.1:343这个服务连接失败了,这是自然的,因为本来就没有这个服务,因为监控检查的后台服务发现这个服务有问题,那么会在后端选择列表中去除这个选项,如果所有后端都失败了,那么第30行的select方法就会返回None, 如果unwrap的话导致代理服务崩溃,但是这个崩溃不会导致整个服务暂停。所以生产的代码尽量不要使用unwrap, 官方示例不过是打个样而已,切记切记。

当服务启动之后没有退出,就说明启动成功了,这时我们就可以测试一下负载均衡了。

curl 127.0.0.1:10080

Service

在上面的代码中一个重要概念就是Service,这也是Pingora的一个比较核心的概念,官方的示意图如下

                               ┌───────────┐
                    ┌─────────>│  Service  │
                    │          └───────────┘
┌────────┐          │          ┌───────────┐
│ Server │──Spawns──┼─────────>│  Service  │
└────────┘          │          └───────────┘
                    │          ┌───────────┐
                    └─────────>│  Service  │
                               └───────────┘

Server是指整个代码,这个Server主要负责所有Service的生命周期管理以及整个程序的生命周期管理,比如创建新的服务,比如热升级程序等。

Service有点类似nginx中的server, 每个Service都是独立的。当然了,也可以共享一些东西,但是这个应该不是最佳实践,多线程处理共享状态很讨厌的。

Service主要是指我们的代理服务,这个代理服务的主要功能示意图如下。

当然不止代理服务啦,比如还有后台服务,我们可以通过实现Service trait来实现任何我们希望的后台服务,最终会由tokio通过spawn方法创建异步任务。

┌────────────┐          ┌─────────────┐         ┌────────────┐
│ Downstream │          │    Proxy    │         │  Upstream  │
│   Client   │─────────>│             │────────>│   Server   │
└────────────┘          └─────────────┘         └────────────┘

也就是接受客户端的请求然后发送给后端(upstream),很简单是吧,高屋建瓴的看轮廓总是这样的,不要一下钻得太深,这样会看不到全貌和丢失信心的。

Listeners

每个服务自然要监听一些端口的啦,监听的部分被抽象成了Listeners, 一般来说我们会监听http请求和https请求,代码分别如下。

监听所有IP的10080 TCP端口(HTTP请求)

lb.add_tcp("0.0.0.0:10080");

监听所有IP的10443 TCP端口(HTTPS请求)

监听HTTPS请求肯定是需要TLS证书的,所以第一步是创建对应的证书,创建的步骤在代码之外完成。

有兴趣的可以参考我的这篇文章创建SSL证书: https://youerning.top/post/mkssl

let cert_path = format!("{}/tests/keys/server.crt", env!("CARGO_MANIFEST_DIR"));
let key_path = format!("{}/tests/keys/key.pem", env!("CARGO_MANIFEST_DIR"));

let mut tls_settings =
pingora_core::listeners::TlsSettings::intermediate(&cert_path, &key_path).unwrap();
tls_settings.enable_h2();
lb.add_tls_with_settings("0.0.0.0:10443", None, tls_settings);

一般来说,我们做7层负载就监听80和443端口,如果你愿意的话,你可以监听很多不同的端口,比如81,82,83等。

HttpPeer

至此,我们可以设置好自己的监听端口,可以等待流量进来了,那么怎么连接后端服务呢? 以HTTP协议(或者说TCP协议)连接还是说先TLS握手? 这个部分就靠我们实现的upstream_peer方法了,它会返回一个HttpPeerHttpPeer就定义了必要的设置。

比如我们代码中的这样。

let peer = Box::new(HttpPeer::new(upstream, false, "".to_string()));

HttpPeer::new需要三个参数,第一个是后端,第二个是否使用TLS配置, 即后端是否是HTTPS, 第三个后端使用的域名(SNI), 因为后端是HTTPS所以肯定是要验证一下对方的证书是否合法的啦。

如果后端是HTTP, 那么第二个和第三个参数就像上面这样写吧。

小结

虽然Service还包含了其他的部分,但是我觉得作为快速入门,上面这些知识大概够了,所以先这样吧。

编译/运行

难过的是pingora编译可能会遇到一些问题,因为涉及的系统的底层组件比较多或者说要求尽可能高的性能,所以没有简单的项目那么好编译,下面是我遇到的一些问题以及解决办法,大家可以参考一下。

值得注意的是: windows环境没办法编译哦,除非是通过wls或者虚拟机之类的方法

编译建议安装开发环境, 比如centos7使用以下命令

yum groupinstall -y "Development Tools"

错误1

--- stderr
  Can't locate IPC/Cmd.pm in @INC (@INC contains: 省略相关路径)

对应解决办法

yum install perl-IPC-Cmd

错误2

CMake Error at CMakeLists.txt:17 (cmake_minimum_required):
    CMake 3.10 or higher is required.  You are running version 2.8.12.2

对应解决办法

安装高于3.10以上版本的CMake

  1. 使用包管理工具安装, 比如Centos7使用命令yum install cmake3安装
  2. 自己编译CMake提供的二进制包, 下载地址: https://cmake.org/download/

错误3

error: failed to run custom build command for `libz-ng-sys v1.1.15`

  --- stderr
  thread 'main' panicked at '
  failed to execute command: Permission denied (os error 13)

对应解决办法

切换成root用户并将cmake3做软链接,软链接命令如下

# 没有安装cmake3的话就先安装一下, yum install cmake3
ln /bin/cmake3 /bin/cmake

小结

报错的核心信息一般集中在stderr, 遇到了本文没遇到的错误就自行搜索一下吧,或者去提一个issue。

总结

Pingora自然还没到成熟的时候,但是值得一试,可以在不重要的服务上试试水,它已经提供了足够多的脚手架来完成搭建常见的负载均衡服务了,或者说反向代理,或者说网关。至于说,开箱即用怕是还要一段时间。

参考链接