Doris 自定义函数之C++ UDF详解

文摘   2024-10-26 00:00   重庆  

  导读   本文主要分享 Apache Doris 1.2版本之前如何构建 C++ UDF。

全文目录:

  1. 背景信息

  2. 环境信息

  3. IDE准备

  4. C++ UDF 开发流程

  5. 自定义TIME_TO_SEC函数

  6. 常见问题

  7. 总结


一、背景信息

【Apache Doris】自定义函数之JAVA UDF详解 发布后,有小伙伴表示由于各种因素还无法升级至 >= 1.2.0 的Doris版本,可否来个 1.2.0 之前的 C++ UDF 详解?安排!

C++自定义UDF函数主要适用于1.2.0版本之前,用户需要的分析能力 Doris 并不具备的场景,例如Tableau通过固化SQL直连Doris查询时出现部分函数不兼容问题。用户可以自行根据自己的需求,实现自定义的函数,并且通过 UDF 框架注册到 Doris 中,来扩展 Doris 的能力,并解决用户分析需求。

二、环境信息
1.硬件信息
  • CPU:48C

  • 内存:256G

2.软件信息
  • 系统:CentOS

  • Apache Doris版本:0.15

三、IDE准备

C++编程可以选择Visual Studio、CLion或Code::Blocks等IDE。

本文选择轻量级的Code::Blocks演示,环境准备好后来个传统美学:
四、C++ UDF 开发流程

1.源码准备   

以官方源码为例。

git clone https://github.com/apache/
incubator-doris/tree/branch-0.15/contrib/udf/src/udf_sample

2.文件上传

根据官文将文件放置对应的路径下,注意修改CMakeLists.txt文件,以官文内容为主。
├── thirdparty
│ │── include
│ │ └── udf.h
│ └── lib
│   └── libDorisUdf.a
└── udf_samples
  ├── CMakeLists.txt
  ├── uda_sample.cpp
  ├── uda_sample.h
  ├── udf_sample.cpp
  └── udf_sample.h

3.文件编译

#进入build文件夹
cd /opt/doris/udf/udf_samples/build

#
生成Makefile
cmake ../

#
生成对应动态库
make

#
编译结果
├── thirdparty
├── udf_samples
  └── build
    └── src
      └── udf_samples
        ├── libudasample.so
        └── libudfsample.so

4.服务搭建

由于<1.2版本的doris client需要http服务获取so动态库,故需搭建nginx或其它http服务(过程省略)。
#安装部署nginx步骤省略
 server {
        listen       8088;
        server_name  localhost;

        location /udf {
          alias   /opt/doris/udf;
        }
        
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
         root   html;
        }
   }

5.函数使用

创建 UDF 函数:
CREATE FUNCTION 
MyADD00(INT,INT
RETURNS INT PROPERTIES ( 
"symbol" = "_ZN9doris_udf666",
"object_file" = "http://nginx_ip:8088/
udf/udf_samples/build/src/udf_samples/libudfsample.so"
 );
使用UDF函数:

五、自定义 TIME_TO_SEC 函数

TIME_TO_SEC:函数实现传入一个时间参数,将其时间部分转换成秒的UDF。

1.源码开发 & 实现一   

(1) 主函数
//time_to_sec 的语法格式
//  TIME_TO_SEC(time)
//语法格式说明
//time:传入时间,如果传入了日期部分,也不会管,只将时间部分转换成秒
//重点:是指将传入的时间转换成距离当天00:00:00的秒数,00:00:00为基数,等于 0 秒

#include <iostream>
#include <string>
#include <regex>
using namespace std;

int time_to_sec(string text)
{
    // clear other str
    regex r("^((?![0-9]{2}:[0-9]{2}:[0-9]{2}).)*");
    string time = regex_replace(text, r, "");
    cout << time << endl;

    // handle abnormal
    if(time.length() != 8)
        return NULL;

    // get hh mm ss
    int HH = atoi(time.substr(0,2).c_str());
    int MM = atoi(time.substr(3,2).c_str());
    int SS = atoi(time.substr(6,2).c_str());

    // return sum sec
    return HH*3600 + MM*60 + SS;
}

int main()
{
    cout<<time_to_sec("1987-01-01 00:39:38")<<endl;
    return 0;
}
(2) UDF头文件
#pragma once

#include "udf.h"
#include <bits/stdc++.h>

namespace doris_udf {

IntVal TIME_TO_SEC(FunctionContext* context, const StringVal& time);

/// --- Prepare / Close Functions ---
/// ---------------------------------

/// The UDF can optionally include a prepare function. 
/// The prepare function is called
/// before any calls to the UDF to evaluate values.
void AddUdfPrepare
(FunctionContext* context, FunctionContext::FunctionStateScope scope)
;

/// The UDF can also optionally include a close function. 
/// The close function is called
/// after all calls to the UDF have completed.
void AddUdfClose
(FunctionContext* context, FunctionContext::FunctionStateScope scope)
;

}
(3) UDF源文件
#include "time_to_sec.h"

namespace doris_udf {

IntVal TIME_TO_SEC(FunctionContext* context, const StringVal& time) {
    // handle null
    if (time.is_null) {
        return IntVal::null();
    }

    // clear other str
    using namespace std;
    const string timestr((char *)time.ptr);
    const regex r("^((?![0-9]{2}:[0-9]{2}:[0-9]{2}).)*");
    const string replace_str = "";
    string hms_time = regex_replace(timestr, r, replace_str);

    // handle str abnormal
    if(hms_time.length() != 8) {
        return IntVal::null();
    }

    // get hh mm ss
    int HH = atoi(hms_time.substr(0,2).c_str());
    int MM = atoi(hms_time.substr(3,2).c_str());
    int SS = atoi(hms_time.substr(6,2).c_str());

    // return sum sec
    return HH*3600 + MM*60 + SS;
}

/// --- Prepare / Close Functions ---
/// ---------------------------------
void AddUdfPrepare
(FunctionContext* context, FunctionContext::FunctionStateScope scope) 
{}
void AddUdfClose
(FunctionContext* context, FunctionContext::FunctionStateScope scope) 
{}
}
(4) 小结
不建议使用,doris对其中的regex相关函数并不友好,会直接导致be core:

2.源码开发 & 实现二   

(1) 主函数
//time_to_sec 的语法格式
//  TIME_TO_SEC(time)
//语法格式说明
//time:传入时间,如果传入了日期部分,也不会管,只将时间部分转换成秒
//重点:是指将传入的时间转换成距离当天00:00:00的秒数,00:00:00为基数,等于 0 秒

#include <iostream>
#include <string>
#include <regex>
usingnamespacestd;

int time_to_sec(string text)
{
   
// clear other str
   
string segSign = ":";
   
string::size_type pos1 = text.find(segSign);

   
if(pos1 == string::npos)
      
cout << "没找到!" << endl;
   
else
      
cout << "找到了!下标:" << pos1<<endl;

    
string time = text.substr(pos1-2,8);
    
cout << time << endl;

    
// handle abnormal
    
if(time.length() != 8)
       
returnNULL;

    
// get hh mm ss
    
int HH = atoi(time.substr(0,2).c_str());
    
int MM = atoi(time.substr(3,2).c_str());
    
int SS = atoi(time.substr(6,2).c_str());

    
// return sum sec
    
return HH*3600 + MM*60 + SS;
}

int main()
{
    
cout<<time_to_sec("1987-01-01 00:39:38")<<endl;
    
return0;
}
(2) UDF头文件
#pragma once

#include "udf.h"
#include <bits/stdc++.h>

namespace doris_udf {

IntVal TIME_TO_SEC(FunctionContext* context, const StringVal& time);

/// --- Prepare / Close Functions ---
/// ---------------------------------

/// The UDF can optionally include a prepare function. 
/// The prepare function is called
/// before any calls to the UDF to evaluate values.
void AddUdfPrepare
(FunctionContext* context, FunctionContext::FunctionStateScope scope)
;

/// The UDF can also optionally include a close function. 
/// The close function is called
/// after all calls to the UDF have completed.
void AddUdfClose
(FunctionContext* context, FunctionContext::FunctionStateScope scope)
;

}
(3) UDF源文件
#include "time_to_sec.h"

namespace doris_udf {

IntVal TIME_TO_SEC(FunctionContext* context, const StringVal& time) {
    // handle null
    if (time.is_null) {
        return IntVal::null();
    }

    // clear other str
    using namespace std;
    string timestr((char *)time.ptr);
    string segSign = ":";
    string::size_type pos = timestr.find(segSign);
    string hms_time;
    if(pos == string::npos)
        return IntVal::null();
     else
        hms_time = timestr.substr(pos-2,8);

    // handle str abnormal
    if(hms_time.length() != 8) {
        return IntVal::null();
    }

    // get hh mm ss
    int HH = atoi(hms_time.substr(0,2).c_str());
    int MM = atoi(hms_time.substr(3,2).c_str());
    int SS = atoi(hms_time.substr(6,2).c_str());

    // return sum sec
    IntVal result;
    result.val = HH*3600 + MM*60 + SS;
    return {result.val};
}

/// --- Prepare / Close Functions ---
/// ---------------------------------
void AddUdfPrepare
(FunctionContext* context, FunctionContext::FunctionStateScope scope) 
{}
void AddUdfClose
(FunctionContext* context, FunctionContext::FunctionStateScope scope) 
{}
}
(4) 小结
基本完全使用字符串的API实现,简单高效并且兼容性较好,最终选定实现二。

3.编译结果   

4.函数使用   

创建 UDF 函数:
CREATE FUNCTION 
TIME_TO_SEC(String
RETURNS INT PROPERTIES ( 
"symbol" = "_ZN9doris_udf999,
"
object_file" = "http://nginx_ip:8088/libtime_to_sec.so" );
原先不兼容TIME_TO_SEC的Tableau固化SQL,现在可以正常运行。
六、常见问题

1.ROS问题   

make时出现ROS问题; 
注意:需要在CMakeFiles.txt头部加一条SET(CMAKE_CXX_FLAGS “-std=c++0x”)命令解决。
2.路径问题   
将CMakeFiles.txt的相对路径都调整为绝对路径或新增路径变量。

七、总结

自定义 C++ UDF 的使用与普通的函数方式一致,唯一的区别在于,内置函数的作用域是全局的,而 UDF 的作用域是 DB 内部; 

1.2后的新版本不建议使用原生C++ UDF,因为兼容性较差、GLIBC一升级就没法用了;建议使用JAVA UDF。

最后,愿各位看官们在2024新的一年里:身体健康、阖家欢乐、心想事成!



往期推荐

大数据平台开发规范示例

【Apache Doris】Manager 极致丝滑地运维管理

【Apache Doris】如何一键实现MySQL万表整库同步?

【Apache Doris】如何实现高并发点查?(原理+实践全析)

Kafka TO Doris 保姆级入门详解

Flink CDC 1.0至3.0回忆录

如何正确地使用ChatGPT(角色扮演+提示工程)

超强满血不收费的AI绘图教程来了(在线Stable Diffusion一键即用)



大数据技能圈
分享大数据前沿技术,实战代码,详细文档
 最新文章