Django ELB 健康检查和持续交付

发布于 2024-08-15 22:49:53 字数 9405 浏览 13 评论 0

使用 Amazon Web Services 部署 web 应用的一个强大的手段是使用一个 弹性负载均衡器(Elastic Load Balancer) (ELB) 来平衡 EC2 实例的“Auto Scaling Group” (ASG) 之间的请求的平衡。和水平扩展一样,这种设置允许自动 canary (又名 blue-green) 部署,其中,新应用版本作为一个新的 ASG 进行部署,这会用新的替换现存的 EC2 实例;一种所谓的“不可改变的基础设施”的做法。

这样的过程依赖于 ELB “健康检查”请求来测试新的 EC2 实例准备好了接收生产流量(这样,老的实例就可以终止了)。对于 canary 部署,健康检查的准确性是非常重要的:误报导致带入生产的挂掉的应用引发问题,宕机时间和其他忧伤的事情。

Octopus Energy 上,我们以这种方式部署了 Django 应用, 作为由 Hashicorp 出色的 Atlas 服务协作的连续的输送管道的一部分:

  1. CircleCI,我们的持续集成服务,当主应用上的测试通过时,会打包应用,然后上传压缩包到 Atlas 上。
  2. 然后,使用一组上传配置(例如,Puppet 清单和模块),Atlas 采用 Packer 来创建一个新的 Amazon Machine Image (AMI)。
  3. 接着,使用 Terraform,Atlas 部署该 AMI 到生产。如上所述,Terraform 将新的 AMI 带到生产,创建一个新的 ASG,然后加载使用这个新的 AMI 的配置。

过去几个月里,该过程的不断发展,已经凸显了正确进行健康检查的重要性。下面是一些提示。使用和 uWSGI 和 NGINX 一起运行的一个样例 Django 应用,但大多数的建议转化为其他框架和 HTTP 服务器。

虽然本文名义上是关于健康检查,但是 TLDR 是那种你可以使用 Hashicorp 的产品构建伟大的东西。特别是,如果你使用 AWS,并且之前没有使用 Terraform —— 那么,今天是时候了。

一个健康检查 Django 视图

我们的 ELB 健康检查在 Terraform 中配置如下:


    resource "aws_elb" "web" {

        ...

        health_check {
            # Where to make health check requests to
            target = "HTTP:80/health"

            # How often to make health check requests (in seconds)
            interval = 15

            # Number of checks before instance is declared healthy
            healthy_threshold = 2

            # Number of checks before instance is declared unhealthy
            unhealthy_threshold = 10

            # Number of seconds to wait for a healthcheck response
            timeout = 5
        }
    }

换句话说,当两个到 /health 的 HTTP 请求返回一个 200 状态(5s 内),那么一个 EC2 实例被认为是健康的。

让我们从简单的开始:


    # urls.py
    from . import views

    urlpatterns = (
        url(r'^health', views.health),
    )


    # views.py
    from django import http

    def health(request):
        return http.HttpResponse()

使用 NGINX 直接返回健康检查请求的响应,而不用麻烦 uWSGI,这样本来是更容易的。但是,通过更深入一层,并且让 Django 应用来响应,我们会获得更多好处。通过这样做,可以避免一些类问题,因为当 uWSGI 无法启动该 Python 应用时,健康检查失败。

不健康的实例不能运行 Python 应用

按照 12 因子应用指导原则 ,我们的 EC2 实例是无状态的,并且从环境变量中读取它们的配置。这由 Upstart 设置,获取由 consul 模板管理的配置文件:


    # /etc/init/uwsgi

    # Ensure the uWSGI process doesn't start until 
    # the consul-template process has started.
    start on started consul-template
    stop on runlevel [06]

    respawn

    # This is the file consul-template manages
    env ENV_FILE="/etc/application/env-vars"
    env UWSGI_INI_FILE="/etc/uwsgi.ini"
    env VENV_ROOT="/opt/venv"

    script
        # Set environment variables 
        source $ENV_FILE

        # Apply migrations. Piping the output to logger ensures it get 
        # is included in /var/log/syslog and hence gets forwarded to Loggly.
        sudo -u www-data django-admin migrate --noinput --no-color | logger -t migrations

        # Start uWSGI
        exec $VENV_ROOT/bin/uwsgi --ini $UWSGI_INI_FILE
    end script

settings.py 中使用简单的包装函数,我们确保在缺失/无效配置下,Python 应用无法启动:


    # settings.py

    import os

    def value_from_env(key, default=None):
        """
        Return a env variable, raising an exception if it is not defined
        """
        value = os.environ.get(key, default)
        if value is None:
            error_msg = (
                "No '%s' env variable found. This needs to be set in Consul's "
                "key-value store."
            )
            raise RuntimeError(error_msg % key)
        return value

    # Example config look-up
    SECRET_KEY = value_from_env("SECRET_KEY")

如果在 Consul 中, SECRET_KEY 环境变量无法定义,那么 uWSGI 将不能够启动 Python 应用,而健康检查将会失败。这种做法可以保证如果配置缺失,那么 canary 部署失败。

假设 uWSGI 可以启动 Python 应用,让我们将允许 Django 成功对健康检查进行响应这种设置作为例子。

NGINX

我们在 ELB 和到 EC2 实例的 80 端口的代理请求终止 TLS。对于正常的用户请求,使用 X_FORWARDED_PROTO 头来确保使用 TLS。然而,对于健康检查请求,我们不想要这样,所以使用单独的 location 指令:


    # /etc/nginx/sites-enabled/default

    # uWSGI is configured to use this socket 
    upstream public {
        server unix:///tmp/uwsgi.sock;
    }

    server {
        listen 80 default_server;

        server_name localhost octopus.energy;

        charset utf-8;

        # Allow healthchecks to be made from ELB without X_FORWARDED_PROTO header
        location /health {
            access_log /var/log/nginx/health.log;

            uwsgi_pass public; 
            include /etc/nginx/uwsgi_params;
        }

        location / {
            # Ensure non-TLS requests are redirected
            if ($http_x_forwarded_proto != 'https') {
                rewrite ^ https://$host$request_uri? permanent;
            } 

            uwsgi_pass public; 
            include /etc/nginx/uwsgi_params;
        }
    }

这里,我们使用一个单独的日志文件,因为我们不想让健康检查的请求包含在主访问文件中,因此我们作为一个 JSON 事件流转发到 Loggly(这超级有用)。

允许的主机

ELB 健康检查请求使用 EC2 实例的私有 IP 地址作为主机头,因此我们需要确保这样的请求正确被 Django 应用处理。

对于 NGINX,这不是个问题,因为我们代理到包罗万象的虚拟主机中的 Django 程序(定义的第一个)。

要让 Django 应用正确响应,私有 IP 地址必须在 ALLOWED_HOSTS 设置中,否则 Django 将会反返回一个“400 Bad Request”响应。由于 Web 服务器是短暂的,这个设置需要动态设置,通常通过在启动过程中调用 AWS 内部元数据服务。你可以在 EC2 “user-data”中进行这样一个请求,然后将值写入到一个配置文件中,或者当导入 settings.py 时,调用该元数据服务。前者可能看起来像这样:


    # userdata.sh

    echo "Writing EC2 metadata to files in /etc/aws/"
    mkdir -p /etc/aws/
    ec2metadata --local-ipv4 > /etc/aws/ipv4


    # settings.py

    def value_from_file(filepath, default=None):
        """
        Return a string value from a local file
        """
        if os.path.exists(filepath):
            with open(filepath, "r") as f:
                return f.read().strip()
        return default

    # Read local IP address from file created by EC2 user-data script
    AWS_LOCAL_IP = value_from_file("/etc/aws/ipv4")

    ALLOWED_HOSTS = [AWS_LOCAL_IP, "octopus.energy"]

在这一点上,上面定义的简单的健康检查视图将会愉快地响应请求。现在,让我们扩展健康检查视图的实现。

检查网页正确渲染

你可以使用 Django 测试客户端来在你的网站上运行一个简单的烟雾测试。例如,检查主页负载。


    # check.py
    import logging
    import httplib

    logger = logging.getLogger('health')

    def page_response(path, expected_status=httplib.OK):
        """
        Make an internal (fake) HTTP request to check a page returns the expected
        status code.
        """
        try:
            response = client.get(path)
        except Exception as e:
            logger.error("Error from %s: %s", path, e)
            return False

        result = response.status_code == expected_status
        if not result:
            # Log healthcheck errors to Loggly so we can debug failing deployments
            # where the new instances fail the healthcheck.
            logger.error("Response from %s was %s, not %s",
                         path, response.status_code, status)
        return result

我们可以在我们的视图函数中使用这个辅助器:


    # views.py
    import httplib

    from django import http

    from . import check

    def health(request):
        if not check.page_response('/'):
            return http.HttpResponse(status=httplib.SERVICE_UNAVAILABLE)
        return http.HttpResponse()

检查迁移成功应用

如上所示,当 Upstart 启动 Django 应用时,我们试图应用迁移。如果任何这些迁移失败,我们就不想把这台机器放入生产。因此,我们检查未应用的迁移作为健康检查的一部分:


    # check.py
    import logging

    from django.db import DEFAULT_DB_ALIAS, connections
    from django.db.migrations.loader import MigrationLoader

    logger = logging.getLogger('health')


    def migrations_have_applied():
        """
        Check if there are any migrations that haven't been applied yet
        """
        connection = connections[DEFAULT_DB_ALIAS]
        loader = MigrationLoader(connection)
        graph = loader.graph

        # Count unapplied migrations
        num_unapplied_migrations = 0
        for app_name in loader.migrated_apps:
            for node in graph.leaf_nodes(app_name):
                for plan_node in graph.forwards_plan(node):
                    if plan_node not in loader.applied_migrations:
                        num_unapplied_migrations += 1

        return num_unapplied_migrations == 0

我们扩展的健康检查视图函数现在看起来是这样的:


    # views.py
    import httplib

    from django import http

    from . import check

    def health(request):
        if not check.page_response('/'):
            return http.HttpResponse(status=httplib.SERVICE_UNAVAILABLE)
        if not check.migrations_have_applied():
            return http.HttpResponse(status=httplib.SERVICE_UNAVAILABLE)
        return http.HttpResponse()

好了,就这样:用于 Ddjango 应用的有效的健康检查视图。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

夜巴黎

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

忆伤

文章 0 评论 0

眼泪也成诗

文章 0 评论 0

zangqw

文章 0 评论 0

旧伤慢歌

文章 0 评论 0

qq_GlP2oV

文章 0 评论 0

旧时模样

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文