选择HTTP反向代理时的尝试和错误

大家好!

今天,我们想谈谈Ostrovok.ru酒店预订服务团队如何解决微服务增长的问题,其任务是与我们的供应商交换信息。 关于他的经历告诉了Ostrovok.ru DevOps团队负责人。


最初,微服务很小,并且执行以下功能:

  • 接受本地服务的请求;
  • 向合作伙伴提出要求;
  • 规范响应;
  • 将结果返回给查询服务。

但是,随着时间的流逝,服务随着合作伙伴的数量和对他们的请求而增长。

随着服务的增长,各种问题开始出现。 不同的提供程序提出了自己的规则:有人限制最大连接数,有人将客户端限制在白名单中。

结果,我们必须解决以下问题:

  • 最好有几个固定的外部IP地址,以便您可以将它们提供给合作伙伴,以将其添加到白名单中,
  • 与所有供应商只有一个连接池,以便在扩展我们的微服务时,连接数量保持最小,
  • 终止SSL并将keepalive放在一个位置,从而减轻合作伙伴本身的负担。

他们想了很久,立即想知道该选择什么:Nginx或Haproxy。
最初,钟摆转向Nginx,因为与HTTP / HTTPS相关的大多数问题我都借助它来解决,并且始终对结果感到满意。

该方案很简单:在Nginx上向我们的新代理服务器发出了一个请求,请求的域名格式为<partner_tag>.domain.local ,在Nginx中有一个map ,其中<partner_tag>对应于合作伙伴的地址。 从map中获取了一个地址,并对该地址进行了proxy_pass

这是一个示例map ,我们使用它来解析域并从列表中选择上游:

 ###     : <tag>.domain.local map $http_host $upstream_prefix { default 0; "~^([^\.]+)\." $1; } ###      map $upstream_prefix $upstream_address { include snippet.d/upstreams_map; default http://127.0.0.1:8080; } ###   upstream_host    upstream_address map $upstream_address $upstream_host { default 0; "~^https?://([^:]+)" $1; } 

这是“ snippet.d/upstreams_map ”的样子:
 “one” “http://one.domain.net”; “two” “https://two.domain.org”; 

这里我们有server{}

 server { listen 80; location / { proxy_http_version 1.1; proxy_pass $upstream_address$request_uri; proxy_set_header Host $upstream_host; proxy_set_header X-Forwarded-For ""; proxy_set_header X-Forwarded-Port ""; proxy_set_header X-Forwarded-Proto ""; } } # service for error handling and logging server { listen 127.0.0.1:8080; location / { return 400; } location /ngx_status/ { stub_status; } } 

一切都很酷,一切正常。 如果没有任何细微差别,可以结束本文。

使用proxy_pass时,通常,使用不带有keepalive的HTTP / 1.0协议,请求直接转到所需地址,并在响应完成后立即关闭。 即使我们proxy_http_version 1.1 ,没有上游( proxy_http_version )也不会改变。

怎么办 首先想到的是让所有供应商进入上游,服务器将成为我们所需供应商的地址,并在map保留"tag" "upstream_name"

添加另一个map以解析该方案:

 ###     : <tag>.domain.local map $http_host $upstream_prefix { default 0; "~^([^\.]+)\." $1; } ###      map $upstream_prefix $upstream_address { include snippet.d/upstreams_map; default http://127.0.0.1:8080; } ###   upstream_host    upstream_address map $upstream_address $upstream_host { default 0; "~^https?://([^:]+)" $1; } ###   ,       https,    ,    -  http map $upstream_address $upstream_scheme { default "http://"; "~(https?://)" $1; } 

并使用标签名称创建upstreams
  upstream one { keepalive 64; server one.domain.com; } upstream two { keepalive 64; server two.domain.net; } 

对服务器本身进行了少许修改,以考虑到方案,并使用上游名称代替地址:

 server { listen 80; location / { proxy_http_version 1.1; proxy_pass $upstream_scheme$upstream_prefix$request_uri; proxy_set_header Host $upstream_host; proxy_set_header X-Forwarded-For ""; proxy_set_header X-Forwarded-Port ""; proxy_set_header X-Forwarded-Proto ""; } } # service for error handling and logging server { listen 127.0.0.1:8080; location / { return 400; } location /ngx_status/ { stub_status; } } 

太好了 该解决方案有效,在每个上游添加keepalive指令,设置proxy_http_version 1.1 ,-现在我们有了一个连接池,一切正常。

这次,您绝对可以完成文章,然后去喝茶。 还是不行

确实,当我们喝茶时,其中一个供应商可能会更改同一域(例如,亚马逊)下的IP地址或地址组,因此其中一个供应商可能会在我们茶会的高峰期倒下。

好吧,该怎么办? Nginx有一个有趣的细微差别:在重新加载期间,它可以将upstream内部的服务器清醒到新地址并向其发送流量。 一般来说,也是一种解决方案。 每5分钟投入cron reload nginx并继续喝茶。

但是在我看来,这似乎仍然是一个一般的决定,所以我开始对Haproxy产生疑问。

Haproxy可以指定dns resolvers和配置dns cache 。 因此,如果Haproxy中的条目已过期,则Haproxy将更新dns cache如果更改,则替换上游的地址。

太好了! 现在由设置决定。

这是Haproxy的简短配置示例:

 frontend http bind *:80 http-request del-header X-Forwarded-For http-request del-header X-Forwarded-Port http-request del-header X-Forwarded-Proto capture request header Host len 32 capture request header Referer len 128 capture request header User-Agent len 128 acl host_present hdr(host) -m len gt 0 use_backend %[req.hdr(host),lower,field(1,'.')] if host_present default_backend default resolvers dns hold valid 1s timeout retry 100ms nameserver dns1 1.1.1.1:53 backend one http-request set-header Host one.domain.com server one--one.domain.com one.domain.com:80 resolvers dns check backend two http-request set-header Host two.domain.net server two--two.domain.net two.domain.net:443 resolvers dns check ssl verify none check-sni two.domain.net sni str(two.domain.net) 

一切似乎都正常了,这一次。 我对Haproxy唯一不满意的是配置描述的复杂性。 您需要构建大量文本才能在上游添加一个文本。 但是懒惰是进步的动力:如果您不想写相同的东西,请编写生成器。

我已经从Nginx获得了一个映射,格式为"tag" "upstream" ,所以我决定以它为基础,解析并基于这些值生成haproxy后端。

 #! /usr/bin/env bash haproxy_backend_map_file=./root/etc/haproxy/snippet.d/name_domain_map haproxy_backends_file=./root/etc/haproxy/99_backends.cfg nginx_map_file=./nginx_map while getopts 'n:b:m:' OPT;do case ${OPT} in n) nginx_map_file=${OPTARG} ;; b) haproxy_backends_file=${OPTARG} ;; m) haproxy_backend_map_file=${OPTARG} ;; *) echo 'Usage: ${0} -n [nginx_map_file] -b [haproxy_backends_file] -m [haproxy_backend_map_file]' exit esac done function write_backend(){ local tag=$1 local domain=$2 local port=$3 local server_options="resolvers dns check" [ -n "${4}" ] && local ssl_options="ssl verify none check-sni ${domain} sni str(${domain})" [ -n "${4}" ] && server_options+=" ${ssl_options}" cat >> ${haproxy_backends_file} <<EOF backend ${tag} http-request set-header Host ${domain} server ${tag}--${domain} ${domain}:${port} ${server_options} EOF } :> ${haproxy_backends_file} :> ${haproxy_backend_map_file} while read tag addr;do tag=${tag//\"/} [ -z "${tag:0}" ] && continue [ "${tag:0:1}" == "#" ] && continue IFS=":" read scheme domain port <<<${addr//;} unset IFS domain=${domain//\/} case ${scheme} in http) port=${port:-80} write_backend ${tag} ${domain} ${port} ;; https) port=${port:-443} write_backend ${tag} ${domain} ${port} 1 esac done < <(sort -V ${nginx_map_file}) 

现在,我们所需要做的就是在nginx_map中添加一个新主机,启动生成器并准备好haproxy配置。

今天可能就这些了。 本文更多地涉及介绍性文章,并且致力于选择解决方案并将其集成到当前环境中的问题。

在下一篇文章中,我将向您详细介绍使用Haproxy时遇到的陷阱,哪些指标对监控很有用,以及应该在系统中进行哪些优化以最大程度地发挥服务器性能。

谢谢大家的关注,再见!

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


All Articles