在某个系统中,在Postgis中计算某些特殊点的距离时特别慢,比正常慢了40几倍,刚开始一直百思不得其解。中启乘数科技的工程师通过debug数据库程序,发现是CentOS7.X的glibc中的数学库libm中三角函数cos计算某些值特别慢的问题。
由于涉及最终客户的一些信息,我们这里就不给出客户的具体SQL了。我们自己造一个SQL来重现此问题。
建一个存储过程:
CREATE OR REPLACE FUNCTION test_distance(arg_p1 geometry, arg_p2 geometry)RETURNS void AS$BODY$DECLAREv_dist double precision;BEGINFOR i IN 1..10000 LOOPv_dist := ST_DistanceSphere (arg_p1, arg_p2);END LOOP;END;$BODY$LANGUAGE plpgsql VOLATILE;
上面的存储过程是重复计算两个点之间的距离1万次。
我们随便计算两个点的距离,如下:
postgres=# SELECT test_distance(ST_GeometryFromText('POINT(0 0)', 4326), ST_GeometryFromText('POINT(100 45)', 4326));test_distance---------------(1 row)Time: 36.038 mspostgres=# SELECT test_distance(ST_GeometryFromText('POINT(0 0)', 4326), ST_GeometryFromText('POINT(30 30)', 4326));test_distance---------------(1 row)Time: 33.948 ms
可以看出上面的时间只需要30~40ms。
但是当我们换一个特殊的点“POINT(119.297915 42.042785)”时发现非常慢,居然要1.5秒:
postgres=# SELECT test_distance(ST_GeometryFromText('POINT(0 0)', 4326), ST_GeometryFromText('POINT(119.297915 42.042785)', 4326));test_distance---------------(1 row)Time: 1548.751 ms (00:01.549)
通过gdb去debug PostgreSQL数据库,发现是运行函数sphere_distance慢:
double sphere_distance(const GEOGRAPHIC_POINT *s, const GEOGRAPHIC_POINT *e){struct timeval tv_begin,tv_end;int64_t my_usec;gettimeofday(&tv_begin,NULL);double d_lon = e->lon - s->lon;double cos_d_lon = cos(d_lon);double cos_lat_e = cos(e->lat);double sin_lat_e = sin(e->lat);double cos_lat_s = cos(s->lat);double sin_lat_s = sin(s->lat);double a1 = POW2(cos_lat_e * sin(d_lon));double a2 = POW2(cos_lat_s * sin_lat_e - sin_lat_s * cos_lat_e * cos_d_lon);double a = sqrt(a1 + a2);double b = sin_lat_s * sin_lat_e + cos_lat_s * cos_lat_e * cos_d_lon;gettimeofday(&tv_end,NULL);my_usec = (int64_t)tv_end.tv_sec * 1000000LL + (int64_t)tv_end.tv_usec - ((int64_t)tv_begin.tv_sec * 1000000LL + (int64_t)tv_begin.tv_usec);g_my_cnt += my_usec;return atan2(a, b);}
进一步debug发现是运行上面的三角函数cos慢。也就是cos函数对于某些特殊的值时非常慢。
为此我们在SQL中直接执行cos函数,发现对于一些特殊值来说会非常慢。
先造测试表:
create table test01(f float8);insert into test01 select 0.73 from generate_series(1, 10000) as seq;
这时运行cos函数会非常快:
postgres=# explain analyze select sum(cos(f)) from test01;QUERY PLAN----------------------------------------------------------------------------------------------------------------Aggregate (cost=197.55..197.56 rows=1 width=8) (actual time=2.628..2.628 rows=1 loops=1)-> Seq Scan on test01 (cost=0.00..146.70 rows=10170 width=8) (actual time=0.017..1.229 rows=10000 loops=1)Planning Time: 0.075 msExecution Time: 2.650 ms(4 rows)Time: 3.084 ms
当我们把表中的值改成特殊的值0.73378502495808418时,就会非常慢:
postgres=# update test01 set f=0.73378502495808418;UPDATE 10000Time: 23.088 mspostgres=# explain analyze select sum(cos(f)) from test01;QUERY PLAN----------------------------------------------------------------------------------------------------------------Aggregate (cost=385.67..385.68 rows=1 width=8) (actual time=369.834..369.835 rows=1 loops=1)-> Seq Scan on test01 (cost=0.00..286.78 rows=19778 width=8) (actual time=0.934..2.461 rows=10000 loops=1)Planning Time: 0.072 msExecution Time: 369.859 ms(4 rows)Time: 370.771 ms
进一步定位发现就是三角函数cos对于一些特殊的值非常慢。写一个C的测试程序testcos.c:
#include <stdio.h>#include <stdint.h>double test_cos(double lat){double cos_lat_e = cos(lat);return cos_lat_e;}int main(int argc, char * argv[]){int i;double dis;double lat;int flag = atoi(argv[1]);int cnt = atoi(argv[2]);uint64_t u64lat;if (flag){/*慢*///lat = 0.73378502495808418;u64lat=0x3FE77B2ABB8FAA1C;//临时//u64lat=0x3FE77B2ABB8FBA1C;}else{ /*快*///lat = 0.73378502670341339;u64lat=0x3FE77B2ABC7F8A6C;}lat = *(double *) &u64lat;//printf("size of double=%d\n", sizeof(lat));//u64lat = *(uint64_t *)⪫printf("u64lat=%lX\n", u64lat);printf("lat=%.15f\n", lat);for (i=0; i<cnt; i++){dis = test_cos(lat);}return 0;}
注意上面把cos计算放在一个单独的函数中,避免编译器把多次循环给优化掉。
编译:
gcc testcos.c -o testcos -lm
[codetest@pgdev gistest]$ time ./testcos 1 50000lat=0.733785024958084real 0m1.853suser 0m1.852ssys 0m0.000s[codetest@pgdev gistest]$ time ./testcos 0 50000lat=0.733785026703413real 0m0.002suser 0m0.000ssys 0m0.001s
testcos中第一个参数为1是慢的情况,第一个参数为0是快的情况。
从上面的测试可以看出不同的值,cos函数执行时间相差上千倍。
上面的测试时是在CentOS7.6上测试的。然后我们又测试了操作系统Rocky Linux8.6,发现没有这个慢的问题。
所以基本断定是glibc的数学库cos的问题。通过查询网上的信息,说这是一个bug:
同时查看glibc中也就是一些特别的值做sin或cos计算时,发现误差过大,然后做了更精确的计算,导致慢了很多。
在CentOS7.X下的glibc版本为glibc-2.17,可以编译升级glibc到2.28的版本,就没有此问题了。注意替换glibc需要小心操作,不小心操作很容易导致操作系统无法启动的问题。
如果不想替换整个操作系统的glibc,可以只替换PostgreSQL程序,让其链接到新版本glibc的数学动态库:
编译glibc-2.28,假设编译完了glib-2.28安装在/home/codetest/glibc-2.28,把postgres依赖的动态库libm.so.6指向glibc-2.28中的libm.so
patchelf --replace-needed libm.so.6 /home/codetest/glibc-2.28/lib/libm-2.28.so /mypg/pgsql/bin/postgres
实际上此问题并不是PostgreSQL数据库的问题,也不是PostGIS的问题,而是glibc-2.17版本的问题,在低版本的glibc中,计算一些特殊值的sin或cos时会非常慢,需要特别注意。
glibc是非常底层的库,当这些底层的库有一些不易发现的缺陷时,会导致一些很奇怪的问题。从此例可以看出,随着国产化的深入需要更多的关心一些底层的技术,才能有效保证计算机系统的安全稳定运行。