我们SRE工作日的6个实用故事



现代Web基础结构由许多用于各种目的的组件组成,这些组件具有明显的联系,并且相互之间的联系并不十分紧密。 当运行使用不同软件堆栈的应用程序时,这一点尤其明显,随着微服务的出现,它实际上在每个步骤中都开始发生。 外部因素(第三方API,服务等)被添加到常规的“乐趣”中,这使本来已经很困难的情况变得更加复杂。

通常,即使将这些应用程序与通用的体系结构思想和解决方案结合在一起,要消除它们中的不寻常问题,通常也要走遍下一个陌生的领域。 这些问题是否发生只是时间问题。 这些是本文专门针对我们最新实践的示例。 演员:Golang,Sentry,RabbitMQ,nginx,PostgreSQL等。

历史第一 Golang和HTTP / 2


运行对Web应用程序执行许多HTTP请求的基准测试会导致意外结果。 在基准测试过程中,一个简单的Go应用程序转到了位于ingress / openresty之后的另一个Go应用程序。 启用HTTP / 2后,对于某些请求,我们将收到代码400的错误;要了解此行为的原因,我们从链的最远端删除了Go应用程序,并在Ingress中创建了一个简单位置,该位置始终返回200。该行为未更改!

然后决定在Kubernetes环境之外的其他硬件上重现脚本。 结果是生成了一个Makefile,借助它启动了两个容器:一个是Apache中的基准-进入nginx的基准,另一个是Apache中的基准。 两者都使用自签名证书来侦听HTTP / 2。 最终操作时间请参见此存储库

使用concurrency=200运行基准测试:

1.1。 Nginx:

 Completed 0 requests Completed 1000 requests Completed 2000 requests Completed 3000 requests Completed 4000 requests Completed 5000 requests Completed 6000 requests Completed 7000 requests Completed 8000 requests Completed 9000 requests ----- Bench results begin ----- Requests per second: 10336.09 Failed requests: 1623 ----- Bench results end ----- 

1.2。 阿帕奇:

 … ----- Bench results begin ----- Requests per second: 11427.60 Failed requests: 0 ----- Bench results end ----- 

我们假设这里的重点是在Apache中不太严格的HTTP / 2实现。

让我们尝试concurrency=1000

2.1。 Nginx:

 … ----- Bench results begin ----- Requests per second: 11274.92 Failed requests: 4205 ----- Bench results end ----- 

2.2。 阿帕奇:

 … ----- Bench results begin ----- Requests per second: 11211.48 Failed requests: 5 ----- Bench results end ----- 

同时,我们注意到结果并非每次重现 :某些启动顺利通过。

在Golang项目的github上搜索问题导致了#25009#32441 。 通过它们,我们进入PR 903 :默认情况下在Go中禁用HTTP / 2!

在不深入研究上述Web服务器的体系结构的情况下解释基准测试结果非常困难。 在特定情况下,为指定服务禁用HTTP / 2就足够了。

历史2号 旧的symfony和哨兵


在其中一个项目中,非常旧版本的symfony PHP框架(v2.3)仍在运行。 在工具箱中将旧的Raven客户端和PHP自写类附加到该工具包上,这使调试变得有些复杂。

在将Kubernetes中的一项服务转移到Sentry(用于跟踪该项目应用程序中的错误)之后,事件突然停止了。 为了重现此行为,我们使用了Sentry网站上的示例,采用了两个选项并从Sentry设置中复制了DSN。 从视觉上看,一切正常:错误消息(据说)是一个接一个地发送的。

JavaScript检查选项:

 <!DOCTYPE html> <html> <body> <script src="https://browser.sentry-cdn.com/5.6.3/bundle.min.js" integrity="sha384-/Cqa/8kaWn7emdqIBLk3AkFMAHBk0LObErtMhO+hr52CntkaurEnihPmqYj3uJho" crossorigin="anonymous"> </script> <h2>JavaScript in Body</h2> <p id="demo">A Paragraph.</p> <button type="button" onclick="myFunction()">Try it</button> <script> Sentry.init({ dsn: 'http://33dddd76e9f0c4ddcdb51@sentry.kube-dev.test//12' }); try { throw new Error('Caught'); } catch (err) { Sentry.captureException(err); } </script> </body> </html> 

同样在Python中:

  from sentry_sdk import init, capture_message init("http://33dddd76e9f0c4ddcdb51@sentry.kube-dev.test//12") capture_message("Hello World") # Will create an event. raise ValueError() 

但是,他们没有进入哨兵。 发送消息时,会产生发送消息的错觉,因为客户端会立即为该问题生成一个哈希。

结果,该问题得以非常简单地解决:事件发送到HTTP,而Sentry服务仅侦听HTTPS。 提供了从HTTP到HTTPS的重定向,但是旧客户端(symfony端的代码)无法遵循重定向,这些天默认情况下您不希望这样做。

历史第3号。 RabbitMQ和第三方代理


在一个项目中,Evotor云用于连接收银机。 实际上,它可以作为代理使用:来自Evotor的POST请求直接通过通过WebSocket连接实现的STOMP插件直接发送到RabbitMQ。

其中一名开发人员使用Postman发出了测试请求,并收到了200 OK的预期响应,但是,通过云进行的请求导致意外的405 Method Not Allowed

200 OK
 source: kubernetes namespace: kube-nginx-ingress host: kube-node-2 pod_name: nginx-2bpt7 container_name: nginx stream: stdout app: nginx controller-revision-hash: 5bdbfd564 pod-template-generation: 25 time: 2019-09-10T09:42:50+00:00 request_id: 1271dba228f0943ab2df0196ff0d7f67 user: client address: 100.200.300.400 protocol: HTTP/1.1 scheme: http method: POST host: rmq-review.kube-dev.client.domain path: /api/queues/vhost/queue.gen.eeeeffff111:1.onlinecassa:55556666/get request_query: referrer: user_agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36 content_kind: cacheable namespace: review ingress: stomp-ws service: rabbitmq service_port: stats vhost: rmq-review.kube-dev.client.domain location: / nginx_upstream_addr: 10.127.1.1:15672 nginx_upstream_bytes_received: 2538 nginx_upstream_response_time: 0.008 nginx_upstream_status: 200 bytes_received: 757 bytes_sent: 1254 request_time: 0 status: 200 upstream_response_time: 0 upstream_retries: 0 

405方法不允许
 source: kubernetes namespace: kube-nginx-ingress host: kube-node-1 pod_name: nginx-4xx6h container_name: nginx stream: stdout app: nginx controller-revision-hash: 5bdbfd564 pod-template-generation: 25 time: 2019-09-10T09:46:26+00:00 request_id: b8dd789604864c95b4af499ed6805630 user: client address: 200.100.300.400 protocol: HTTP/1.1 scheme: http method: POST host: rmq-review.kube-dev.client.domain path: /api/queues/vhost/queue.gen.ef7fb93387ca9b544fc1ecd581cad4a9:1.onlinecassa:55556666/get request_query: referrer: user_agent: ru.evotor.proxy/37 content_kind: cache-headers-not-present namespace: review ingress: stomp-ws service: rabbitmq service_port: stats vhost: rmq-review.kube-dev.client.domain location: / nginx_upstream_addr: 10.127.1.1:15672 nginx_upstream_bytes_received: 134 nginx_upstream_response_time: 0.004 nginx_upstream_status: 405 bytes_received: 878 bytes_sent: 137 request_time: 0 status: 405 upstream_response_time: 0 upstream_retries: 0 

来自Evotor的Tcpdump请求
 200.100.300.400.21519 > 100.200.400.300: Flags [P.], cksum 0x8e29 (correct), seq 1:879, ack 1, win 221, options [nop,nop,TS val 2313007107 ecr 79097074], length 878: HTTP, length: 878 POST /api/queues//vhost/queue.gen.ef7fb93387ca9b544fc1ecd581cad4a9:1.onlinecassa:55556666/get HTTP/1.1 device-model: ST-5 device-os: android Accept-Encoding: gzip content-type: application/json; charset=utf-8 connection: close accept: application/json x-original-forwarded-for: 10.11.12.13 originhost: rmq-review.kube-dev.client.domain x-original-uri: /api/v2/apps/e114-aaef-bbbb-beee-abadada44ae/requests x-scheme: https accept-encoding: gzip user-agent: ru.evotor.proxy/37 Authorization: Basic X-Evotor-Store-Uuid: 20180417-73DC-40C9-80B7-00E990B77D2D X-Evotor-Device-Uuid: 20190909-A47B-40EA-806A-F7BC33833270 X-Evotor-User-Id: 01-000000000147888 Content-Length: 58 Host: rmq-review.kube-dev.client.domain {"count":1,"encoding":"auto","ackmode":"ack_requeue_true"}[!http] 12:53:30.095385 IP (tos 0x0, ttl 64, id 5512, offset 0, flags [DF], proto TCP (6), length 52) 100.200.400.300:80 > 200.100.300.400.21519: Flags [.], cksum 0xfa81 (incorrect -> 0x3c87), seq 1, ack 879, win 60, options [nop,nop,TS val 79097122 ecr 2313007107], length 0 12:53:30.096876 IP (tos 0x0, ttl 64, id 5513, offset 0, flags [DF], proto TCP (6), length 189) 100.200.400.300:80 > 200.100.300.400.21519: Flags [P.], cksum 0xfb0a (incorrect -> 0x03b9), seq 1:138, ack 879, win 60, options [nop,nop,TS val 79097123 ecr 2313007107], length 137: HTTP, length: 137 HTTP/1.1 405 Method Not Allowed Date: Tue, 10 Sep 2019 10:53:30 GMT Content-Length: 0 Connection: close allow: HEAD, GET, OPTIONS 

curl发出的tcpdump请求
 777.10.74.11.61211 > 100.200.400.300:80: Flags [P.], cksum 0x32a8 (correct), seq 1:397, ack 1, win 2052, options [nop,nop,TS val 734012594 ecr 4012360530], length 396: HTTP, length: 396 POST /api/queues/%2Fvhost/queue.gen.ef7fb93387ca9b544fc1ecd581cad4a9:1.onlinecassa:55556666/get HTTP/1.1 Host: rmq-review.kube-dev.client.domain User-Agent: curl/7.54.0 Authorization: Basic = Content-Type: application/json Accept: application/json Content-Length: 58 {"count":1,"ackmode":"ack_requeue_true","encoding":"auto"}[!http] 12:40:11.001442 IP (tos 0x0, ttl 64, id 50844, offset 0, flags [DF], proto TCP (6), length 52) 100.200.400.300:80 > 777.10.74.11.61211: Flags [.], cksum 0x2d01 (incorrect -> 0xfa25), seq 1, ack 397, win 59, options [nop,nop,TS val 4012360590 ecr 734012594], length 0 12:40:11.017065 IP (tos 0x0, ttl 64, id 50845, offset 0, flags [DF], proto TCP (6), length 2621) 100.200.400.300:80 > 777.10.74.11.61211: Flags [P.], cksum 0x370a (incorrect -> 0x6872), seq 1:2570, ack 397, win 59, options [nop,nop,TS val 4012360605 ecr 734012594], length 2569: HTTP, length: 2569 HTTP/1.1 200 OK Date: Tue, 10 Sep 2019 10:40:11 GMT Content-Type: application/json Content-Length: 2348 Connection: keep-alive Vary: Accept-Encoding cache-control: no-cache vary: accept, accept-encoding, origin 

训练有素的工程师的眼睛立即看到了区别:

  • curl: POST /api/queues/%2Fclient…
  • Evotor: POST /api/queues//client…

问题是,在一种情况下,一个无法理解的(对于RabbitMQ) //vhost ,而在另一种情况下, %2Fvhost ,这是在以下情况下的预期行为:

 # rabbitmqctl list_vhosts Listing vhosts ... /vhost 

在有关此主题的RabbitMQ项目中,开发人员说明:

我们不会替换%-encoding。 这是URL路径编码的标准方法,并且已经存在了很长时间。 假设基于HTTP的工具中的%-encoding将会消失,因为即使是最流行的框架,假设此类URL路径是“恶意的”也是近视和幼稚的。 可以将默认虚拟主机名更改为任何值 (例如,不使用斜杠或任何其他需要%-encoding的字符),并且至少使用RabbitMQ的Pivotal BOSH版本时,无论如何在部署时都将默认虚拟主机删除。

该问题无需我们的工程师进一步参与即可解决(与他们联系后,在Evotor方面)。

历史第4号 PostgreSQL中的基因


PostgreSQL有一个非常有用的索引,经常被遗忘。 这个故事始于对应用程序刹车的抱怨。 在最近的文章中,我们已经给出了分析此类情况时的近似工作流示例。 在这里,我们的APM- Atatus-显示了以下图片:



在上午10点,应用程序花费在处理数据库上的时间会增加。 不出所料,原因在于DBMS响应缓慢。 对我们来说,分析查询,识别问题区域和“悬挂”索引是可以理解的例程。 我们使用的okometer很多帮助:既有用于监视服务器状态的标准面板,又有快速构建我们自己的功能的能力-输出有问题的指标:



CPU负载图表明其中一个数据库已100%加载。 怎么了 新的PostgreSQL面板将提示:



问题的原因是显而易见的-CPU的主要使用者:

 SELECT u.* FROM users u WHERE u.id = ? & u.field_1 = ? AND u.field_2 LIKE '%somestring%' ORDER BY u.id DESC LIMIT ? 

考虑到有问题的查询的工作计划,我们发现按表的索引字段进行过滤的选择范围太大:数据库通过idfield_1接收了超过7万行,然后在其中搜索一个子字符串。 事实证明, LIKE字符串的LIKE会迭代大量文本数据,这会导致查询执行速度严重下降,并增加CPU负载。

在这里您可以正确地注意到,没有排除体系结构问题(需要应用程序逻辑更正,甚至需要全文本引擎...),但是没有时间进行返工,但是应该在15分钟之前就可以很快地进行工作。 同时,搜索词实际上是一个标识符(为什么不在单独的字段中呢?..),该标识符生成行单位。 实际上,如果我们可以在此文本字段上编写索引,则所有其他字段将变得不必要。

当前的最终解决方案是为field_2添加一个GIN索引。 那就是今天的英雄-同样的“精灵”。 简而言之,GIN是一种在全文搜索中表现出色的索引,可以从质量上加快索引的速度。 例如,您可以在这份精彩的资料中阅读有关它的更多信息。



如您所见,此简单的操作可以消除额外的负担,并为客户节省金钱。

历史5号 在Nginx中缓存s3


长期以来,与S3兼容的云存储一直被许多项目所采用的技术牢牢地列在技术清单中。 如果您需要用于站点或神经网络数据的可靠图像存储,那么Amazon S3是一个不错的选择。 存储的可靠性和高数据可用性(并且不需要“围墙”)令人着迷。

但是,有时是为了省钱-因为通常S3的费用用于请求和流量-一个好的解决方案是在存储设备前面安装一个缓存代理服务器。 这种方法可以降低成本,例如,涉及到用户头像,每个页面上都有很多。

似乎比使用nginx并设置具有缓存,重新验证,后台更新和其他二十一点的代理要容易得多? 但是,与其他地方一样,有些细微差别...

这种具有缓存的代理的大致配置如下所示:

 proxy_cache_key $uri; proxy_cache_methods GET HEAD; proxy_cache_lock on; proxy_cache_revalidate on; proxy_cache_background_update on; proxy_cache_use_stale updating error timeout invalid_header http_500 http_502 http_503 http_504; proxy_cache_valid 200 1h; location ~ ^/(?<bucket>avatars|images)/(?<filename>.+)$ { set $upstream $bucket.s3.amazonaws.com; proxy_pass http://$upstream/$filename; proxy_set_header Host $upstream; proxy_cache aws; proxy_cache_valid 200 1h; proxy_cache_valid 404 60s; } 

总的来说,它可以正常工作:显示图片,使用缓存一切正常……但是,AWS S3客户端出现了问题。 特别是, aws-sdk-php的客户端停止工作。 对nginx日志的分析表明,上游返回了HEAD请求的403代码,并且响应包含一个特定错误: SignatureDoesNotMatch 。 当我们看到nginx向上游发出GET请求时,一切就绪。

事实是S3客户端对每个请求进行签名,然后服务器检查此签名。 在简单代理的情况下,一切正常:请求未更改地到达服务器。 但是,启用缓存后,nginx将开始优化后端的工作并将HEAD请求替换为GET。 逻辑很简单:最好先检索并保存整个对象,然后再从缓存中检索所有HEAD请求。 但是,在我们的情况下,由于该请求已签名,因此无法修改。

本质上有两种解决方案:

  1. 不要通过代理来驱动S3客户端;
  2. 如果需要,请关闭proxy_cache_convert_head选项,然后将$request_method添加到缓存键。 在这种情况下,nginx将“按原样”发送HEAD请求并分别缓存对它们的响应。

历史6号 DDoS和Google用户内容


直到星期天晚上才预示着问题-突然! -边缘服务器上的缓存失效队列尚未增加,这将流量提供给实际用户。 这是一个非常奇怪的症状:毕竟,缓存是在内存中实现的,并不与硬盘驱动器绑定。 在使用的体系结构中刷新缓存是一种廉价的操作,因此仅在负载很高的情况下才会出现此错误。 相同服务器开始通知出现500个错误(下图中的红线尖峰)这一事实证实了这一点。



如此急剧的飙升导致CPU超支:



快速分析表明,请求没有到达主域,但是从日志中可以清楚地看到它们位于默认的虚拟主机中。 一路上,事实证明,有许多美国用户来到了俄罗斯资源。 这种情况总是立即引起疑问。

从nginx日志中收集数据后,我们发现我们正在处理某个僵尸网络:

 35.222.30.127 US [15/Sep/2019:21:40:00 +0300] GET "http://example.ru/?ITPDH=XHJI" HTTP/1.1 301 178 "http://example.ru/ORQHYGJES" "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729)" "-" "upcache=-" "upaddr=-" "upstatus=-" "uplen=-" "uptime=-" spdy="" "loc=wide-closed.example.ru.undef" "rewrited=/?ITPDH=XHJI" "redirect=http://www.example.ru/?ITPDH=XHJI" ancient=1 cipher=- "LM=-;EXP=-;CC=-" 107.178.215.0 US [15/Sep/2019:21:40:00 +0300] GET "http://example.ru/?REVQSD=VQPYFLAJZ" HTTP/1.1 301 178 "http://www.usatoday.com/search/results?q=MLAJSBZAK" "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)" "-" "upcache=-" "upaddr=-" "upstatus=-" "uplen=-" "uptime=-" spdy="" "loc=wide-closed.example.ru.undef" "rewrited=/?REVQSD=VQPYFLAJZ" "redirect=http://www.example.ru/?REVQSD=VQPYFLAJZ" ancient=1 cipher=- "LM=-;EXP=-;CC=-" 107.178.215.0 US [15/Sep/2019:21:40:00 +0300] GET "http://example.ru/?MPYGEXB=OMJ" HTTP/1.1 301 178 "http://engadget.search.aol.com/search?q=MIWTYEDX" "Mozilla/5.0 (Windows; U; Windows NT 6.1; en; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729)" "-" "upcache=-" "upaddr=-" "upstatus=-" "uplen=-" "uptime=-" spdy="" "loc=wide-closed.example.ru.undef" "rewrited=/?MPYGEXB=OMJ" "redirect=http://www.example.ru/?MPYGEXB=OMJ" ancient=1 cipher=- "LM=-;EXP=-;CC=-" 

日志中跟踪了一个可以理解的模式:

  • 真正的用户代理;
  • 对带有随机GET参数的根URL的请求,以避免进入缓存;
  • 引荐来源网址表示该请求来自搜索引擎。

我们收集地址并验证它们的隶属关系-它们都属于googleusercontent.com ,具有两个子网(107.178.192.0/18和34.64.0.0/10)。 这些子网包含GCE虚拟机和各种服务,例如页面翻译。

幸运的是,攻击并没有持续那么长时间(大约一个小时),并逐渐减少了。 Google内部的保护性算法似乎已经奏效,因此该问题已“自行解决”。

这次攻击没有破坏性,但对未来提出了有用的问题:

  • 为什么抗DDoS不起作用? 使用了外部服务,我们向其发送了相应的请求。 但是,有很多地址...
  • 将来如何保护自己免受此侵害? 在我们的情况下,甚至可以选择关闭地理区域的访问权限。

聚苯乙烯


另请参阅我们的博客:

Source: https://habr.com/ru/post/zh-CN471892/


All Articles