最近在搞faas平台,也试了各大云厂商的产品,效果都不是很理想。和我心目中的faas想去甚远。
和小伙伴们吹完牛逼,心有所感,写下这篇文章,时间跨度较长,故事的结局可以直接看尾语。
faas最突出的特点只有两个,方便快捷,低成本。
方便快捷是开发者使用faas最大的原因。faas平台能够提供全套的底层服务,包括日志,流量,监控,测试集成,各种触发方式,各种语言支持,云编辑器,开发框架等等。作为开发者,理想的faas状态是,写好了业务代码发个版就用。
低成本是对开发者和平台供应商是双向的。使用faas的开发者一般都不会开发非常复杂的东西,换而言之就是要省成本,省钱。用的时候花钱,不用的时候0成本。对于平台提供商而言,每一个提供出来的函数服务都要足够省成本,资源利用率要足够合理。faas是可以按照请求收费的,如果成本不够低,平台商就要赔出裤衩子。最理想的状态下,faas可以将实例缩成0,不消耗任何资源。
基于物理机或k8s,这种很好理解。将每个函数做成一个pod跑起来就行。阿里云用的就是这种方法。将函数放到ECS里执行。不好的地方是代价高昂,而且冷启动巨慢,根据官方的数据,最快启动也要在300ms以上。所以实际生产上不可能将实例缩成0
基于KVM虚拟化技术,已知AWS用的这种方法,其底层框架就是rust写的Firecracker。个人感觉这种还不如docker。如果docker是按秒启动,它就得按照分钟启动。不过隔离性到是很好,技术也很成熟。
基于wasm(WebAssembly),这种是底层有个Wasm Runtime,它能够将各种语言编好的特定二进制包运行起来,启动速度在毫秒级。这种方式还不太成熟,就算是其中最优秀的的wasmtime(bytecodealliance的作品)框架,也仅仅支持少数语言。据说字节有用这种方法,"强如死月"就在字节搞这种东西,在火山引擎上感受了一把,嗯~ 好像闻不对味。
如果说wasm还不成熟,那为啥不用动态库的方法那,动态库发展折磨多年,技术早就成熟的一匹,各种稀奇古怪的语言都支持。而且可以把动态库做成热加载模式。类似wasm,让函数和服务分离,在需要的时候再加载进来。
我们整体构想如下。
关说不练假把式,下面来动手实操一下。
用任何语言制作一个标准C接口的动态库。
当然也可以用我这个现成的。
https://github.com/woshihaoren4/wd_passport/tree/main/cdylib
这个库是用rust写的,用来做鉴权和认证用的。特点是证书内敛,且定期自动升级。可以把公钥分发出去做分布式验证。
主要封了两个标准C的接口一个是签名,一个是验证。用c语言写大概就是这种
int sign(char* data,long timestamp,char * sign) // 用于签名 并将签名串写到入参sign中
int verify(cahr * data, char * sign,long timestamp) //验证 返回0表示成功 其他则失败
我本人用的mac所以,使用.dylib结尾的文件。其他系统所需动态库 需要跨平台编译,我这里质变仪了.so的。并附上该包跨平台编译方法。
//查看rust支持的平台
rustc --print target-list
//下载平台支持库,这里加一个linux的musl编译支持
rustup target add x86_64-unknown-linux-musl
//因为上面用的musl编译,所以需要安装musl
brew install FiloSottile/musl-cross/musl-cross
//在项目的cargo.toml文件中,设置编译目标 参考:https://doc.rust-lang.org/reference/linkage.html?highlight=crate-type#linkage
//staticlib:静态库 cdylib:c标准动态库
[lib]
name = "lib_name"
crate-type = ["staticlib", "cdylib"]
//linker 创建./cargo/config.toml 文件并写入如下内容
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
//编译
cargo build --target x86_64-unknown-linux-musl
//如果报错 找不到musl-gcc 则执行如下命令
ln -s /usr/local/bin/x86_64-linux-musl-gcc /usr/local/bin/musl-gcc
这里使用libloading库动态加载动态库
将上面编译好的库放到下面的代码中执行。
完整代码
use std::ffi::{c_longlong, CString};
use std::os::raw::{c_char, c_int};
fn main() {
let start = std::time::Instant::now();
let result = call_dynamic().expect("运行出错");
let use_time = start.elapsed();
println!("success :{} 总用时:{}毫秒", result, use_time.as_millis());
}
fn call_dynamic() -> Result<u32, Box<dyn std::error::Error>> {
unsafe {
let start = std::time::Instant::now();
let lib = libloading::Library::new("./src/libwd_passport.dylib")?;
let sign_func: libloading::Symbol<
unsafe extern "C" fn(
data: *const c_char,
timestamp: c_longlong,
sign: *const c_char,
) -> c_int,
> = lib.get(b"sign")?;
let verify_func: libloading::Symbol<
unsafe extern "C" fn(
data: *const c_char,
sign: *const c_char,
timestamp: c_longlong,
) -> c_int,
> = lib.get(b"verify")?;
let use_time = start.elapsed();
println!("启动用时:{}微秒", use_time.as_micros());
let start = std::time::Instant::now();
let data = CString::from_vec_unchecked(Vec::from("hello world"));
let sign = CString::from_vec_unchecked(vec![1; 128]);
let result = sign_func(data.as_ptr(), 1866248975, sign.as_ptr());
assert_eq!(result,0,"签名失败");
let use_time = start.elapsed();
println!("签名用时:{}微秒", use_time.as_micros());
let start = std::time::Instant::now();
let result = verify_func(data.as_ptr(), sign.as_ptr(), 1866248975);
assert_eq!(result,0,"验签失败");
let use_time = start.elapsed();
println!("验证用时:{}微秒", use_time.as_micros());
return Ok(0);
}
}
运行结果:
这里可以看到,启动时间在亚毫秒级。因为代码中是从文件中加载,文件加载和读取是需要花费一定时间的,当多次运行测试代码后,会发现启动时间小于毫秒,因为系统会缓存文件。
签名用时较久是因为第一次会初始化一些配置。如果将代码中的单次签名改成多次,会发现签名用时大概在2毫秒左右。动态库用的是rsa sha256算法。
高密度部署一直是faas比较头疼的事,动态库方法固然能够极大降低资源成本,但是会让开发变得即为复杂。
领导评估这个方案直接就是不通过,理由是开发周期过长,不能快速上线。
最终方案还是用k8s,套个壳就叫faas了。嗯~ 这很具有z国特色。
截止到我写下这篇博文23年2月份,项目开发了6个月了。实际上两个月的时候我们就开发完了,三个月的时候上线了第一版。截止目前,项目组辞掉了外包,遣散了其他部门借调的人(包括我),只留下大猫两三只继续开发。