首页
产品
CLup:PostgreSQL高可用集群平台CData高性能数据库云一体机CBackup数据库备份恢复云平台CPDA高性能双子星数据库机
解决方案
数据库专业技术服务全栈式PostgreSQL解决方案Oracle分布式存储化数据库云
文章
客户及伙伴
中启开源
关于我们
公司简介 联系我们
中启开源

背景

在某个系统中,在Postgis中计算某些特殊点的距离时特别慢,比正常慢了40几倍,刚开始一直百思不得其解。中启乘数科技的工程师通过debug数据库程序,发现是CentOS7.X的glibc中的数学库libm中三角函数cos计算某些值特别慢的问题。

场景重新

由于涉及最终客户的一些信息,我们这里就不给出客户的具体SQL了。我们自己造一个SQL来重现此问题。

建一个存储过程:

  1. CREATE OR REPLACE FUNCTION test_distance(arg_p1 geometry, arg_p2 geometry)
  2. RETURNS void AS
  3. $BODY$
  4. DECLARE
  5. v_dist double precision;
  6. BEGIN
  7. FOR i IN 1..10000 LOOP
  8. v_dist := ST_DistanceSphere (arg_p1, arg_p2);
  9. END LOOP;
  10. END;
  11. $BODY$
  12. LANGUAGE plpgsql VOLATILE;

上面的存储过程是重复计算两个点之间的距离1万次。

我们随便计算两个点的距离,如下:

  1. postgres=# SELECT test_distance(ST_GeometryFromText('POINT(0 0)', 4326), ST_GeometryFromText('POINT(100 45)', 4326));
  2. test_distance
  3. ---------------
  4. (1 row)
  5. Time: 36.038 ms
  6. postgres=# SELECT test_distance(ST_GeometryFromText('POINT(0 0)', 4326), ST_GeometryFromText('POINT(30 30)', 4326));
  7. test_distance
  8. ---------------
  9. (1 row)
  10. Time: 33.948 ms

可以看出上面的时间只需要30~40ms。

但是当我们换一个特殊的点“POINT(119.297915 42.042785)”时发现非常慢,居然要1.5秒:

  1. postgres=# SELECT test_distance(ST_GeometryFromText('POINT(0 0)', 4326), ST_GeometryFromText('POINT(119.297915 42.042785)', 4326));
  2. test_distance
  3. ---------------
  4. (1 row)
  5. Time: 1548.751 ms (00:01.549)

问题定位

通过gdb去debug PostgreSQL数据库,发现是运行函数sphere_distance慢:

  1. double sphere_distance(const GEOGRAPHIC_POINT *s, const GEOGRAPHIC_POINT *e)
  2. {
  3. struct timeval tv_begin,tv_end;
  4. int64_t my_usec;
  5. gettimeofday(&tv_begin,NULL);
  6. double d_lon = e->lon - s->lon;
  7. double cos_d_lon = cos(d_lon);
  8. double cos_lat_e = cos(e->lat);
  9. double sin_lat_e = sin(e->lat);
  10. double cos_lat_s = cos(s->lat);
  11. double sin_lat_s = sin(s->lat);
  12. double a1 = POW2(cos_lat_e * sin(d_lon));
  13. double a2 = POW2(cos_lat_s * sin_lat_e - sin_lat_s * cos_lat_e * cos_d_lon);
  14. double a = sqrt(a1 + a2);
  15. double b = sin_lat_s * sin_lat_e + cos_lat_s * cos_lat_e * cos_d_lon;
  16. gettimeofday(&tv_end,NULL);
  17. 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);
  18. g_my_cnt += my_usec;
  19. return atan2(a, b);
  20. }

进一步debug发现是运行上面的三角函数cos慢。也就是cos函数对于某些特殊的值时非常慢。
为此我们在SQL中直接执行cos函数,发现对于一些特殊值来说会非常慢。

先造测试表:

  1. create table test01(f float8);
  2. insert into test01 select 0.73 from generate_series(1, 10000) as seq;

这时运行cos函数会非常快:

  1. postgres=# explain analyze select sum(cos(f)) from test01;
  2. QUERY PLAN
  3. ----------------------------------------------------------------------------------------------------------------
  4. Aggregate (cost=197.55..197.56 rows=1 width=8) (actual time=2.628..2.628 rows=1 loops=1)
  5. -> Seq Scan on test01 (cost=0.00..146.70 rows=10170 width=8) (actual time=0.017..1.229 rows=10000 loops=1)
  6. Planning Time: 0.075 ms
  7. Execution Time: 2.650 ms
  8. (4 rows)
  9. Time: 3.084 ms

当我们把表中的值改成特殊的值0.73378502495808418时,就会非常慢:

  1. postgres=# update test01 set f=0.73378502495808418;
  2. UPDATE 10000
  3. Time: 23.088 ms
  4. postgres=# explain analyze select sum(cos(f)) from test01;
  5. QUERY PLAN
  6. ----------------------------------------------------------------------------------------------------------------
  7. Aggregate (cost=385.67..385.68 rows=1 width=8) (actual time=369.834..369.835 rows=1 loops=1)
  8. -> Seq Scan on test01 (cost=0.00..286.78 rows=19778 width=8) (actual time=0.934..2.461 rows=10000 loops=1)
  9. Planning Time: 0.072 ms
  10. Execution Time: 369.859 ms
  11. (4 rows)
  12. Time: 370.771 ms

进一步定位发现就是三角函数cos对于一些特殊的值非常慢。写一个C的测试程序testcos.c:

  1. #include <stdio.h>
  2. #include <stdint.h>
  3. double test_cos(double lat)
  4. {
  5. double cos_lat_e = cos(lat);
  6. return cos_lat_e;
  7. }
  8. int main(int argc, char * argv[])
  9. {
  10. int i;
  11. double dis;
  12. double lat;
  13. int flag = atoi(argv[1]);
  14. int cnt = atoi(argv[2]);
  15. uint64_t u64lat;
  16. if (flag)
  17. {
  18. /*慢*/
  19. //lat = 0.73378502495808418;
  20. u64lat=0x3FE77B2ABB8FAA1C;
  21. //临时
  22. //u64lat=0x3FE77B2ABB8FBA1C;
  23. }
  24. else
  25. { /*快*/
  26. //lat = 0.73378502670341339;
  27. u64lat=0x3FE77B2ABC7F8A6C;
  28. }
  29. lat = *(double *) &u64lat;
  30. //printf("size of double=%d\n", sizeof(lat));
  31. //u64lat = *(uint64_t *)&lat;
  32. printf("u64lat=%lX\n", u64lat);
  33. printf("lat=%.15f\n", lat);
  34. for (i=0; i<cnt; i++)
  35. {
  36. dis = test_cos(lat);
  37. }
  38. return 0;
  39. }

注意上面把cos计算放在一个单独的函数中,避免编译器把多次循环给优化掉。

编译:

  1. gcc testcos.c -o testcos -lm
  1. [codetest@pgdev gistest]$ time ./testcos 1 50000
  2. lat=0.733785024958084
  3. real 0m1.853s
  4. user 0m1.852s
  5. sys 0m0.000s
  6. [codetest@pgdev gistest]$ time ./testcos 0 50000
  7. lat=0.733785026703413
  8. real 0m0.002s
  9. user 0m0.000s
  10. sys 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

  1. 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是非常底层的库,当这些底层的库有一些不易发现的缺陷时,会导致一些很奇怪的问题。从此例可以看出,随着国产化的深入需要更多的关心一些底层的技术,才能有效保证计算机系统的安全稳定运行。