什么是eBPF

什么是eBPF

从Linux Tracing说起

在我们做程序调试和性能分析时候经常需要跟踪(Tracing)程序执行过程,收集执行信息,综合收集到的信息分析程序问题。基于这样的需求,Linux生态便出现了各种各样的工具,我们在编程开发中或多或少已经接触到了这样的工具,比如GDB、perf,甚至自己代码中的Printf也算工具之一。概括来讲,Tracing工具是实现“采集目标程序执行信息目的”的程序。

那么实现Tracing,需要解决什么问题呢?

  1. 数据源在哪里。
  2. 采集方式。

以GDB为例,我们知道GDB调试,需要开发人员编译加特定参数,编译器会在编译结果程序中加入特定Debug信息。这个加参数暴露Debug信息是一个下探针的过程,探针是Tracing数据的来源。然后我执行GDB命令,单步执行、输出状态信息,这是采集和使用Tracing数据的过程。

先加探针,然后采集调试的过程,在线上程序出性能问题的时候,很难实现,而且很多程序也是到线上场景才暴露性能问题的,能不能随时加探针,随时跟踪目标程序呢?操作系统作为应用程序的执行环境,可以拥有上帝视角,借助内核加探针使我们拥有随时加探针的超能力!

Linux Tracing技术不断发展,诞生了各种各样的工具,形成了一个Tracing系统生态。

在Linux内核中已经有了各种各样的探针,按照形态分为:

  • 硬件探针,硬件直接暴露信息,比如CPU cycles数。
  • 静态探针,重新编译内核或者通过加载内核模块形式下的探针,代表技术: Tracepoint。
  • 动态探针,可以动态在内核函数上添加Hook的探针机制,代表技术Kprobe。

简单深入下静态探针和动态探针,假设我们需要Hook一个内核函数,静态探针需要我们改内核代码,在目标函数前后加入我们采集数据的代码,动态探针则是我们动态指定对应函数和Hook脚本就可以了。相比静态探针需要重新编译内核添加新探针,动态探针灵活几乎可以Hook所有内核函数。

基于内核各种各样的探针,诞生了各种工具、库、框架(Kprobe,Tracepoint,LTTng,SystemTap,eBPF等),这些工具、库底层共用内核的探针技术,用户实现一个Tracing目的,可以选择使用不同的工具、库。

为什么是eBPF

eBPF并不是一个新的Tracing探针技术,而是新的Tracing框架技术,以更安全、更高效、更灵活的方式,方便用户实现Linux Tracing!

采用eBPF可以对接几乎已有的所有探针技术,eBPF的灵活形态,方便用户实现特定化需求。

eBPF工作原理

eBPF程序分为两部分:

  • 内核态代码,被加载进内核执行的代码,加载之前需要被验证器校验,确保不会对内核造成伤害。
  • 用户态代码,和我们正常实现一个应用代码没有区别,在用户进程空间执行,但是额外做一些帮助内核态代码load进内核的工作。

内核态代码会被限制可以调用的函数,通常干采集数据的工作,用户态代码调用函数不限,方便实现业务逻辑,那么内核态代码和用户态代码如何沟通数据呢?eBPF提供了bpf_maps,是用户态和内核态共享的数据空间,通过k-v结构共享存取数据。

内核态代码也不同与我们写正常C语言代码,编译成机器码二进制进入内核执行的,而是内核约定的一套Bytecode指令,eBPF的验证器验证ByteCode通过后,eBPF的JIT编译器将ByteCode编译成机器码进入内核执行。这样一套过程,验证确保安全,JIT确保高效。

我们写eBPF内核态代码不能直接写ByteCode(类似汇编)吧?目前Clang实现了将C语言代码编译成eBPF Bytecode的后端,所以可以使用C编写eBPF内核态代码。用户态代码是在eBPF程序用户进程执行,所以可以便随意选择编程语言。

例子:

内核态代码
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>


struct key_t {
u32 prev_pid;
u32 curr_pid;
};

// 创建与用户态代码共享数据的eBPF maps
BPF_HASH(stats, struct key_t, u64, 1024);
int count_sched(struct pt_regs *ctx, struct task_struct *prev) {
struct key_t key = {};
u64 zero = 0, *val;


key.curr_pid = bpf_get_current_pid_tgid();
key.prev_pid = prev->pid;


// could also use `stats.increment(key);`
val = stats.lookup_or_try_init(&key, &zero);
if (val) {
(*val)++;
}
return 0;
}
用户态代码

#!/usr/bin/python
# Copyright (c) PLUMgrid, Inc.
# Licensed under the Apache License, Version 2.0 (the "License")

from bcc import BPF
from time import sleep

# 帮助load 内核态代码
b = BPF(src_file="task_switch.c")
# 指定内核态代码Hook点,指定探针
b.attach_kprobe(event_re="^finish_task_switch$|^finish_task_switch\.isra\.\d$",fn_name="count_sched")

# generate many schedule events
for i in range(0, 100): sleep(0.01)

for k, v in b["stats"].items():
print("task_switch[%5d->%5d]=%u" % (k.prev_pid, k.curr_pid, v.value))

相关链接

阅读全文

在离线业务混部

什么是在离线业务

  • 在线服务:运行时间长,服务流量及资源利用率有潮汐特征,时延敏感,对服务 SLA 要求极高,如消息流 Feed 服务、电商交易服务等。

  • 离线作业:运行时间分区间,运行期间资源利用率较高,时延不敏感,容错率高,中断一般允许重运行,如 Hadoop 生态下的 MapReduce、Spark 作业。

技术门槛

  • 可观测性体系
  • 调度决策

在离线混部的调度决策是决定混部效果的核心,目前主要有几种决策方式:

整机分时复用:在固定的时间点(比如凌晨以后)跑离线作业,白天让出资源给在线服务。这种以时间维度切分的混部方式比较简单易理解,但整体资源利用率提升有限。

资源部分共享:将单机的资源整体划分为在线资源、离线资源以及在离线共享资源,各资源之间隔离,提前划分预留。这种从单机资源维度切分的混部方式比分时复用相对更精细一些,但是需要资源规格较大的机器切分才有意义。

资源完全共享:通过及时准确的资源预测手段、快速响应资源变化的能力,以及一套可以在资源水位发生变化时的服务保障措施,更高效自动化地实现机器资源复用。资源归属不预设,完全依据实时指标决策。

前一种属于静态决策,相对来说对底层可观测性体系的要求、对调度系统的高可用高性能的要求较低。
后两种属于动态决策,在资源利用率的提升上比静态决策更优,但对前述支撑系统要求也更高。

  • 调度执行
  • 资源隔离
  • 任务冲突时的资源保障
  • 服务平行扩缩容能力

业界在离线混部方案

  • 独占内核 + 物理机 + 静态决策

入门级的在离线混部选择,比如物理机运行服务且分时整机腾挪。

  • 独占内核 + 容器 + 动态决策

如果公司研发团队底层技术积累比较少,想快速、安全、低成本地用上在离线混部,先享受部分混部的成本优化红利,则独占内核+ 容器 + 动态决策组合的方案是首选。

  • 共享内核 + 容器 + 动态决策

如果有比较强的研发实力,能够较好解决第二部分中讲到的几乎所有技术门槛,就可以挑战共享内核 + 容器 + 动态决策组合的方案,以追求极致的资源利用率和成本优化效果。

阅读全文

开源函数计算平台Refunc

Serverless介绍

云计算服务从IaaS到PaaS到如今炙手可热的Serverless,服务对象始终是计算资源使用者。Serverless是一种让开发者无需关心基础设施(服务器等),而是专注到应用程序业务逻辑上的计算服务模型。

加州大学伯克利分校在论文中尝试给出Serverless的定义:Serverless computing = FaaS + BaaS。

Serverless 包含两个组成部分 BaaS(后端即服务)和 FaaS(函数即服务)。对象存储、关系型数据库以及 MQ 等基础支撑服务属于BaaS。FaaS为开发人员提供了一种运行应用的抽象方式,在层次上更靠近应用程序开发者。

FaaS理解

FaaS为开发人员提供了一种运行应用的抽象方式,可以在无需管理服务器的情况下响应事件。例如,上载文件可触发自定义代码,从而将文件转码为各种格式。

FaaS通过事件驱动执行,它会随时待命,但不需要任何服务器进程在后台持续运行。当请求到来时,FaaS在毫秒内启动服务并处理各个请求,当请求减少时,FaaS会自动减少服务副本数量甚至关停服务。

动态扩缩容使FaaS在成本效益上颇具弹性空间,提供商可以仅对使用的资源收费,而不对空闲时间收费。

FaaS平台设计

从服务形态看,FaaS平台需要实现几个关键部分:

  • 函数环境与执行
  • 从0到N动态扩缩容
  • 事件驱动框架

函数环境可以分两个点,系统环境和函数依赖,系统环境主要指系统平台、RootFS等,函数依赖是用户代码本身依赖的第三方库等。函数执行需要为用户代码分配计算实体,具体可以是进程、容器、虚拟机、Wasm等等。

动态扩缩容是FaaS平台对函数状态感知到反馈的一个体现,可以收集函数执行环境和依赖的服务指标来实现,比如采集CPU、内存、带宽、IOPS等等指标判断函数负载采取扩缩容行为。

函数执行依靠事件驱动,事件框架是事件采集、传输的关键。所以设计FaaS平台事件框架需要考虑扩展性,事件传输效率,事件采集协议等等问题。

开源函数计算平台Refunc

先上项目地址: https://github.com/refunc/refunc

Refunc是一个基于Kubernetes的开源函数计算平台,架构如下图:

基于Kubernetes开发实现,核心的CRD包括:

  • Funcdef 函数定义
  • Funcinsts 函数实例
  • Xenv 运行环境
  • Trigger 触发器

工作原理概括为,基于Funcdef和Xenv解决函数环境与执行,定义的Trigger收集事件通过基于Nats的事件框架传递到Funcinsts驱动函数执行并返回结果,核心Operator扩展了Kubernetes的HPA实现了从0到N的函数动态扩缩容。

Refunc拥抱AWS Lambda生态,函数Runtime完全兼容Lambda,基于Lambda的Runtime生态支持各种编程语言,平台接口API兼容Lambda核心接口,完全可以使用aws官方cli工具。

阅读全文

2021年度总结

2021年度总结

Q1

  • Kubernetes网络
    • Calico BGP实现研究
  • 应用型负载均衡系统设计
  • 分布式文件系统研究
    • juicefs部分源码研究

Q2

  • 应用型负载均衡系统实现
  • Kubernetes部分源码研究
    • 定制编译k3s
    • kubernetes分享
  • Openresty/Nginx研究
    • openresty lua 模块开发

Q3

  • Linux网络栈研究
    • 防火墙/iptables工具/容器网络深入
  • CI/CD引擎调研
    • Cyclone部分源码研究
  • 容器运行时研究
    • Containerd部分源码研究
    • Runc部分源码研究
  • 容器网路研究
    • CNM/CNI部分实现研究
  • PHP-FPM部分源码研究
    • Web开发/PHP技术分享

Q4

  • CI/CD构建系统设计与实现
  • Serverless/FaaS平台建设
阅读全文

Nginx扩展之Openresty

Nginx模块

在前面文章nginx编译安装中我们提到,nginx模块是提前编译进去的,所以如果要基于nginx做扩展开发,就要相对比较收悉nginx代码结构和编译过程。

虽然未来nginx版本即使支持动态链接库,使用C语言写nginx扩展门槛也同样高。

复习一下,查询编译安装nginx模块信息,使用-V参数即可

nginx -V
nginx version: openresty/1.19.3.2
built by gcc 9.3.1 20200408 (Red Hat 9.3.1-2) (GCC)
built with OpenSSL 1.1.1k  25 Mar 2021
TLS SNI support enabled
configure arguments: --prefix=/usr/local/openresty/nginx ... --with-http_v2_module --without-mail_pop3_module --without-mail_imap_module --without-mail_smtp_module --with-http_stub_status_module --with-http_realip_module --with-http_addition_module --with-http_auth_request_module --with-http_secure_link_module --with-http_random_index_module --with-http_gzip_static_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-threads --with-compat --with-stream --with-http_ssl_module

可以看到在configure arguments中详细列出了我们当前nginx中已经编译进去的模块。

Openresty

使用C并且重新编译nginx,对于我们基于nginx做业务开发,造成了诸多不便,开发测试、上线更新相对麻烦。Openresty的解决方式是首先使用C写了lua-nginx-module,将luajit嵌入了nginx中,打开了扩展的大门,然后业务开发模块可以使用lua脚本语言来开发。

openresty的官方解释为一个基于nginx的可编写脚本的Web平台,除了核心的lua-nginx-moduleopenresty还包含维护了非常多lua编写的第三方模块,开箱即用,很方便我们做业务定制开发。

安装Openresty

官方推荐安装方式为通过软件源二进制包安装,因为编译lua-nginx-module还是非常麻烦的,对于我们使用lua快速实现业务来说,这块精力可以不花,如果要深入openresty可以尝试完全从源码编译安装。

# add the yum repo:
wget https://openresty.org/package/centos/openresty.repo
sudo mv openresty.repo /etc/yum.repos.d/

# update the yum index:
sudo yum check-update

sudo yum install openresty

通过官方源安装openresty非常简单,而且安装之后所有文件都在/usr/local/openresty下面,非常简洁:

$ ls -al
总用量 64
drwxr-xr-x  10 root root  4096 6月   3 11:46 .
drwxr-xr-x. 16 root root  4096 6月   3 11:46 ..
drwxr-xr-x   2 root root  4096 6月   3 11:46 bin
-rw-r--r--   1 root root 22924 6月   1 13:11 COPYRIGHT
drwxr-xr-x   6 root root  4096 6月   3 11:46 luajit
drwxr-xr-x   5 root root  4096 6月   3 11:46 lualib
drwxr-xr-x   6 root root  4096 6月   3 11:46 nginx
drwxr-xr-x   4 root root  4096 6月   3 11:46 openssl111
drwxr-xr-x   3 root root  4096 6月   3 11:46 pcre
drwxr-xr-x   3 root root  4096 6月   3 11:46 site
drwxr-xr-x   3 root root  4096 6月   3 11:46 zlib

注意到这个目录下有opensslpcre两个目录,回顾我们编译安装nginx提到的依赖,可见openresty的源已经帮我们解决好了这个依赖,统一放到当前目录下。

启动Openresty

安装完openresty我们完全可以使用openresty命令替换nginx命令,所有的参数都一样,准备一个conf文件启动。

# mkdir -p /tmp/nginx/logs
# /tmp/nginx/demo.conf

worker_processes  1;

error_log logs/error.log debug;

events {
    worker_connections 1024;
}

daemon off;

http {

    server {
        listen 9090;
        location / {
            content_by_lua_block {
                ngx.say("hello world")
            }
        }
    }

}

# openresty -p /tmp/nginx -c /tmp/nginx/demo.conf

另起终端测试

curl -v http://127.0.0.1:9090
*   Trying 127.0.0.1:9090...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 9090 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:9090
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: openresty/1.19.3.2
< Date: Fri, 18 Jun 2021 09:04:56 GMT
< Content-Type: text/plain
< Transfer-Encoding: chunked
< Connection: keep-alive
<
hello world
* Connection #0 to host 127.0.0.1 left intact

Lua扩展编写

在上面demo.conf中我们已经实现了openresty lua的hello world,值得注意的是我们使用了一个content_by_lua_block指令,这个指令是lua-nginx-module提供的,意思为这个location响应内容通过执行lua产生。

在介绍更多指令之前我们先看一下lua-nginx-module的在nginx中的的流程图:

图中从上到下描述了lua-nginx-module可作用的:启动初始化lua、处理请求rewrite/access、产生响应、记录日志阶段,以及对应阶段可使用的指令。

每一个阶段的*_by_lua指令后面,我们可以引用我们的lua脚本,或者直接写片段的lua代码。对应的我们可以在lua脚本中实现请求当前阶段的业务逻辑,比如可以在access阶段做安全认证等逻辑。

更多指令: https://github.com/openresty/lua-nginx-module#directives

openresty除了实现lua-nginx-module开启了执行lua脚本能力外,还将许多nginx api直接封装进了lua,我们在实现业务逻辑的时候在脚本中可以直接使用,比如在上面demo.conf中我们调用的ngx.say(),便是一个类似print的api。

更多nginx lua api: https://github.com/openresty/lua-nginx-module#nginx-api-for-lua

总结

总的来说,使用openresty来做基于nginx的业务开发,门槛已经大大降低了,几乎不用去熟悉nginx的代码和数据结构。了解lua-nginx-module提供的指令、封装好的api、在nginx中的作用流程,是入坑的开始。

相关项目

  • ingress-nginx-controller

kubernetes社区官方基于lua能力实现的ingress-controller,使用lua主要解决动态upstream的问题。
https://github.com/kubernetes/ingress-nginx

  • kong

基于nginx+lua实现的api-gateway。
https://github.com/kong/kong

阅读全文