我是如何降低数据库80%负载的 -欧洲杯足彩官网

0顶
0踩

我是如何降低数据库80%负载的

2017-08-11 15:29 by 副主编 jihong10102006 评论(0) 有7905人浏览
引用
原文:
作者:
译者:roy

译者注:一个前端开发者介绍了他和他的数据库朋友们是如何降低基于ruby网站数据库负载的故事。以下为译文:

数据库负载可能是个沉默的性能杀手。我一直都在优化我的一个,但我注意到一些随机的查询时间异常,有时会长达15s或更长。虽然我注意到这个现象有些时候了,但直到最近才开始优化我的数据库查询。(并且使用rack mini profiler工具),然后。在这些重要的提升后,平均响应时间在50ms左右,95%在1s以内。但是,我遇到一个讨厌的问题,在24小时内,95%响应时间可能急升到15s或30s并在短时间内超时。本文将介绍我如何查找并解决这个问题。这最终使我的数据库降低了80%的负载。

这是我的响应时间图,我希望移除这些异常峰值。

为了理解为什么这个(或这些)请求是如此的慢,我用了计量工具。在本例中使用了。修改了比例来展示过去12小时内的请求(默认是3小时)。然后聚焦到这些巨大的峰值。这是我看到的

应用或者数据库肯定有些不对劲。在scout的输出里,你可以看到一个查询要38秒才能完成。我试着手工去访问这个页面但是它很快就加载了。所以不会是页面的问题。
很幸运的是我在heroku工作,我立即在我们数据库工程师的slack聊天室里问他们是什么可能的原因引起了性能的下降。他们问我数据库的平均负载。我用的是一个heroku声称它可以承受。我打开了 并寻找 load-avg。 我在那条慢请求时间附件发现这条记录
jun 29 01:01:01 issuetriage app/heroku-postgres: source=database sample#current_transaction=271694354
sample#db_size=4469950648bytes sample#tables=14 sample#active-connections=35
sample#waiting-connections=0 sample#index-cache-hit-rate=0.87073  sample#table-cache-hit-rate=0.47657
sample#load-avg-1m=2.15 sample#load-avg-5m=1.635 sample#load-avg-15m=0.915
sample#read-iops=16.325 sample#write-iops=0 sample#memory-total=15664468kb
sample#memory-free=255628kb sample#memory-cached=14213308kb sample#memory-postgres=549408kb

一般负载在0.2或以下是正常的,但我的应用峰值到了2.15,呦呵!
我已经花了不少时间来优化我的查询时间,所以我对此还是很意外的。一位数据工程师建议我使用

如果你不使用heroku,你可以通过 _pg_stat_statements_ 表来得到同样的数据

当我安装了这个扩展并使用该命令发现一条查询语句占了高达(你猜对了)80%的执行时间。
$ heroku pg:outliers
total_exec_time  | prop_exec_time |   ncalls    |   sync_io_time   |                                                                                       query
------------------ ---------------- ------------- ------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
3790:50:52.62102 | 80.2%          | 100,727,265 | 727:08:40.969477 | select  ? as one from "repos" where lower("repos"."name") = lower($1) and ("repos"."id" != $2) and "repos"."user_name" = $3 limit $4
493:04:18.903353 | 10.4%          | 101,625,003 | 52:09:48.599802  | select count(*) from "issues" where "issues"."repo_id" = $1 and "issues"."state" = $2

这是那个查询语句(方便较小的显示屏幕)
select ?
as one
from "repos"
where lower("repos"."name") = lower($1) and
("repos"."id" != $2) and
"repos"."user_name" = $3
limit $4

对此我感到很奇怪。因为我不记得写过这样的查询语句。我搜索了代码库中含有 lower sql函数的代码但没有找到任何一条。于是我得助于papertrail来看看在现实生产环境中这个语句什么时候被调用的。我找到的第一条记录在一个创建操作中:
started post "/repos" for 131.228.216.131 at 2017-06-29 09:34:59
processing by reposcontroller#create as html
  parameters: {"utf8"=>"✓", "authenticity_token"=>lir3aynog==", "url"=>"https://github.com/styleguidist/react-
  user load (0.9ms)  select  "users".* from "users" where "users".
  repo load (1.1ms)  select  "repos".* from "repos" where "repos".
   (0.9ms)  begin
  repo exists (1.9ms)  select  1 as one from "repos" where lower( $3 limit $4
   (0.5ms)  commit
   (0.8ms)  begin
  reposubscription exists (4.3ms)  select  1 as one from "repo_ns"."user_id" = $2 limit $3
  sql (5.6ms)  insert into "repo_subscriptions" ("created_at",
   (6.1ms)  commit
[activejob] enqueued sendsingletriageemailjob (job id: cbe2b04a-d271
redirected to https://www.codetriage.com/styleguidist/react-
completed 302 found in 39ms (activerecord: 21.9ms)
jun 29 02:35:00 issuetriage heroku/router:  at=info method=post path="/repos" host=www.codetriage.com request_id=5e706722-7668-4980-ab5e-9a9853feffc9 fwd="131.228.216.131" dyno=web.3 connect=1ms service=542ms status=302 bytes=1224 protocol=https

为了简洁,日志的标签被移除了

这有点难读,但你可以看 repo exists右边的查询语句。我查看了那个控制入口函数(reposcontroller#create)并检查了一些可疑方法,但是结果都没问题(例如,都没有调用 sql lower 函数)。那么问题来了,这些查询语句是从哪里来的呢?

最终答案是来自于我的数据模型中的。这行貌似无害的代码承担了我数据库80%的负载。这个 validate 调用是 rails 试图保证两个 repo 记录没有相同的用户名和用户姓名。它没有采用在数据库中强制执行一致性,而是在模型对象上加了一个 before commit的钩子,这样在模型对象写入数据库前,它会查询数据库来确保我们创建一个新 repo 记录的时候没有重复的记录。

在我写这个验证逻辑的时候并没有想太多。看这个验证代码本身也很难相信它居然引发如此大的数据库负载。毕竟我只有大概2000条repo记录。理论上这个验证调用最多调用2000次,对吧?

为了回答这个问题,我重新查找日志并找到另外一处这个sql语句执行的地方。
jun 29 07:00:32 issuetriage app/scheduler.8183:  [activejob] enqueued populateissuesjob (job id: 9e04e63f-a515-4dcd-947f-0f777e56dd1b) to sidekiq(default) with arguments: #>
performing populateissuesjob (uri=#

为了简洁,日志的标签被移除了

这一次这个查询语句不是来自网页动作,而是一个后台作业。当我检查时,我意识到这个验证不止在创建时执行,它还在_任何_记录的修改时执行。即使用户名或用户姓名没有改动,它还是会查询数据库来确保没有重复。

我有一个晚间任务来遍历所有的代码库并且有时会更新他们的记录。事实是后台任务和这个慢网络请求发生在几乎相同的时间。我自己的后台任务使得数据库负载急升,远超一般负载容量。其他普通的对时间敏感的网络请求就因为没有数据cpu时间而被迫等待并超时。

我立刻删除了这个验证并用一个单一索引代替,同时在数据库上加了限制。
class adduniqueindextorepos < activerecord::migration[5.1]
  def change
    add_index :repos, [:name, :user_name], :unique => true
  end
end

现在我们可以确定在数据库里没有两个记录会有相同的用户名/用户名字组合,rails程序也不需要在每次修改记录时去查询数据库。

更不用提rails程序验证存在竞争并且实际上并不保证一致性,最好是在数据库层面确保这些(一致性)事情。

你可能注意到 sql lower 函数并没有在我的单一性索引中出现。在我的应用中,我已经对存储的数据做了规范化处理,所以这个逻辑是多余的。

在删除验证代码并增加单一性索引后,我的应用再也没有出现过30秒以上的请求延时。数据库一直都在0.2 load-avg 或以下 运行。


当我们面对数据库运行变慢时,我们倾向于考虑一个单独的查询语句的性能。我们很少考虑一个或几个查询语句可能相互影响并拖慢整个网站。

在看到 pg:outliers 结果后,我可以在其他几个合适的位置加上索引来减少负载。 例如:

issuetriage::database=> explain analyze select “repos”.* from “repos” where “repos”.”full_name” = ‘schneems/wicked’ limit 1;

query plan

limit (cost=0.00..39297.60 rows=1 width=1585) (actual time=57.885..57.885 rows=1 loops=1)
-> seq scan on repos (cost=0.00..39297.60 rows=1 width=1585) (actual time=57.884..57.884 rows=1 loops=1)
filter: ((full_name)::text = ‘schneems/wicked’::text)
rows removed by filter: 823
total runtime: 57.912 ms
(5 rows)

这里整体执行时间并不是在几秒内,这个并不算好。那个串行化的扫描很快,但并非没有代价。我对 _full_name 加了一个索引,现在它快的要飞起来。同样的查询可以在 1ms 内返回。针对这些调用的所以也帮助我减少了数据库负载。

总结一下:
  • 一个高的 load-avg 会拖慢所有的查询语句,不仅仅是那些慢查询语句。
  • 使用 pg:outlier 来发现那些占用了更多cpu时间的查询语句(如果你使用heroko),如果你使用其他平台,你也可以使用 _pg_stat_statements
  • 使用日志来定位查询语句发生的时间并用 explain analyze 来分析为什么一个查询如此耗时。
  • 你的查询语句的输入很重要并且可能严重影响到查询性能
  • 添加索引,改变数据的存储或者改变程序逻辑来避免异常的查询
  • 如果可能的话,利用数据库来保证数据一致性而不是使用程序代码
事后来看,这是个很简单的错误并且很容易定位和修复,只是要花点时间和使用正确的工具。我注意到那个30s 的请求延时峰值有几个月了,甚至几年。我从没有去深挖原因,因为我原以为这会很麻烦。它也只是每天发生一次,对用户的影响很小。利用正确的工具和我们数据库工程师的建议,我很快就解决了。我不认为我掌握了数据库优化,但至少现在我达到了目标。谢谢你阅读我的数据库负载之旅。
0
0
评论 共 0 条 请登录后发表评论

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 深入java虚拟机pdf英文版

  • http://java.sun.com/docs/books/jvms/second_edition/html/vmspectoc.doc.html

  • 一、虚拟机参数设置文档官方文档 http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html http://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html 非官方文档 http://www.jvmer.com/jvm-xx-�

  • 目录前言1 introduction1.1 a bit of history1.2 the java virtual machine1.3 organization of the specification1.4 notation1.5 feedback2 the structure of the java virtual machine2.1 the class file format2.2 ...

  • 关于java相关外文文献,中英对照,毕设可以用到!希望对大家有所帮助

  • jvm_调优基础(中英文对照版)。。。。。。。。。。。。。。。。。。。。。。。

  • 深入java虚拟机英文版第二版,很清晰,包含目录,只是不能点击标题跳转

  • 深入java虚拟机第2版 清晰的pdf文件,仅供学习参考

  • java se的chm版本api,从1.5到8,英文版,中文版本翻译难免有所偏差,就没有传上来

  • 这是在准备面试的时候,把java虚拟机的相关知识整理的文档。

  • jvm(java虚拟机)的整个流程:发展,运行区域,垃圾回收器,内存分配策略,垃圾收集,jvm分析工具,jvm优化

  • the java® virtual machine specification java se 8 edition。jvm学习文档。高清带目录。

  • jdk8 https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.htmljdk7 https://docs.oracle.com/javase/7/docs/technotes/tools/solaris/java.html官方博客 https://blogs.oracle.com/poonam/==============...

  • jdk api 版本6 版本8 版本9 中文文档及部分英文文档 百度网盘链接:https://pan.baidu.com/s/1vhimfrq59q5xjoj5boqbha 提取码:l2t1

  • http://docs.oracle.com/javase/7/docs/technotes/guides/vm/g1.html garbage-first collector             introduction the garbage-first (g1) garbage collector is f

  • [http://www.javaalmanac.com] - java开发者年鉴一书的在线版本. 要想快速查到某种java技巧的用法及示例代码, 这是一个不错的去处. [http://www.onjava.com] - oreilly的java网站. 每周都有新文章. [http://java.sun.com] - 官方的java开发者网站 - 每周都有新文章发表. [http://www.de

  • 前言   说到学习jvm,其实我本人并不认为学习完以后会对目前工作有什么太大的帮助。但是为了深入了解java体系,使自己在看待问题上能够看到更本质的部分还是必须要学习的。同时对于自己的技术也是一个深入。   闲话少说,这个系列主要是阅读java虚拟机规范的一些知识点的梳理和心得,后续可能还包括经典的《深入java虚拟机》一书的系列。   首先提供一下《java虚拟机规范(java se 7)...

  • java 虚拟机 官方文档 java 虚拟机文档

global site tag (gtag.js) - google analytics
网站地图