于边缘计算场景之内,一种常见的尴尬情形存在:设备端仅仅需要去做一项简单的IP地理位置查询,然而却因为资源方面的限制,具体而言是几十MB的内存、几百MB的存储以及老旧的ARM处理器,就此被迫放弃了“重量级”的方案。
我们公司在近期,协助了某工业互联网团队,进行边缘网关的优化工作,在此期间遇到了典型场景,一批ARMv7工业网关,其内存仅仅只有512MB,却要完成实时解析前端300多个PLC设备的出口IP归属地的任务,以此用于流量调度以及安全策略。传统的做法是部署完整IP库加上HTTP服务,然而针对这种“螺蛳壳里做道场”的环境,必须另寻其他途径。
一、架构取舍:别把“微服务”做成“微胖”
许许多多的开发者,在边缘地带沿用着云端的思路,去运行一个Spring Boot内嵌的IP库。经过实际测试,一个完整的Spring Boot应用,哪怕仅仅只编写了一个接口,在启动之后,其内存占用轻轻松松就突破了200 - 300MB,对于总内存只有512MB的设备而言,这显然是无法接受的,还要预留出一定的余量给数据采集以及协议转换。
我们在实践中参考了分层解耦思路,将IP查询模块拆为三层:
核心原则是,能做成静态库的就不做成动态服务,能够使用C语言的就不接触JVM,能够读取内存的就不读取磁盘。最终,我们运用C语言去编写轻量的CGI或者本地Socket服务,把二进制IP库直接通过mmap映射到内存。
二、IP库轻量化:从MySQL到二进制文件
将传统方案依赖MySQL或者Redis视作过于奢侈之举,我们需求是那种无依赖且能够直接进行加载的二进制文件。
数据进行预先处理:于云端把IP段进行排序之后,生成具有定长记录的二进制文件。每一条记录的结构是这样的:
typedef struct {
uint32_t start_ip; // 起始IP(网络字节序)
uint32_t end_ip; // 结束IP
uint16_t geo_id; // 地理位置ID(指向字符串表)
} ip_record_t;
每一条记录,其字节数为10,具体构成是4加上4再加上2,100万条这样的记录,仅仅大约是10MB。要是只保留国内常用的IP段,那么记录数能够被压缩,压缩之后此记录数在30万条以内,整体体积被控制,控制在3MB左右。
加载的机制是,采用mmap来映射文件,而不是使用read,它是按照需求进行加载的,并且能够实现多进程共享,核心代码的片段是。
int load_ip_db(const char *path) {
int fd = open(path, O_RDONLY);
struct stat st;
fstat(fd, &st);
// mmap整个文件,只读,共享
void *addr = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
close(fd);
g_records = (ip_record_t *)addr;
g_record_count = st.st_size / sizeof(ip_record_t);
return 0;
}
有一个512MB的设备,在其上进行mmap操作,mmap的对象是一个3MB的文件,此时实际的物理内存占用啊,几乎是为0的,因为仅仅是加载访问到的页带来的损耗。
三、接口极简:去掉“中间商”
我们作出选择,将Nginx、Tomcat去除掉,直接采用基于epoll的C语言Socket服务,其他应用借助TCP或者Unix Domain Socket来发送IP字符串,像"8.8.8.8"这样的,服务给出JSON回来。
查询核心:二分查找
const char *ip_to_location(uint32_t ip) {
int lo = 0, hi = g_record_count - 1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (ip < g_records[mid].start_ip) {
hi = mid - 1;
} else if (ip > g_records[mid].end_ip) {
lo = mid + 1;
} else {
return geo_table[g_records[mid].geo_id];
}
}
return "unknown";
}
关键优化:
四、动态更新与“断网自治”
工业现场网络,波动呈现出频繁的状态,就此而言模块必然需要具备离线自治的能力,借鉴KubeEdge的本地自治理念,我们在其中内置了双区,也就是A区与B区的升级机制,云端借助MQTT来下发新的IP库,此新IP库是以Base64分片形式存在的,边缘会将其写入备用分区并且进行校验,以原子性的方式切换符号链接,通过信号触发reload,在整个这样的过程当中业务查询不会出现中断,即便处于断网状态时依然会使用旧库来提供服务。
五、数据源可靠性
于上述架构里,数据的源头对模块上限起到了决定作用。工业网关项目在最终的时候选用了IP数据云,相关原因存在着三个:
适用于离线库的轻量化适配,IP数据云ipdatacloud.com的离线库,能够支持自定义字段输出,可直接生成符合我们二进制格式的文件,进而省去为服务端进行二次清洗的过程。有着高精度与低内存平衡的特性,其国内库省市级准确率超过99%,在体积控制方面表现优秀,mmap后查询延迟处于微秒级。具备增量更新机制,基于时间戳的增量包,每日几千字节,通过MQTT拉取后在本地进行合并,极大地降低了带宽和失败率。六、呈现出的效果以及总结。
以下是不同方案于资源受限设备上测到的对比情况,该设备网关为ARMv7 1.2GHz,且有512MB RAM,是实测呀:
方案二进制体积常驻内存平均查询延迟并发能力
Spring Boot + 文本库
15MB
280MB
5ms
50 TPS
Python Flask + mmap
8MB
45MB
0.3ms
200 TPS
C 语言 + mmap (本文)
2.8MB
4.5MB
7µs
2000+ TPS
最终部署的IP查询模块表现:
边缘计算并非云计算那种简单的复制,而是处于一种“带着镣铐跳舞”的状况。信通院相关报告曾指出,边缘侧数据处理正朝着“敏态”以及“轻量”加以演进。从底层数据源着手精心雕琢,还要从通信协议方面细致打磨,这或许是开发者朝着架构师发展的关键一步。
如果你也在边缘侧遇到类似瓶颈,不妨从数据源轻量化开始尝试。
