千家信息网

如何使用Django和Prometheus以及Kubernetes定制应用指标

发表于:2024-11-12 作者:千家信息网编辑
千家信息网最后更新 2024年11月12日,这篇文章给大家介绍如何使用Django和Prometheus以及Kubernetes定制应用指标,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。这里强调了应用程序定制指标的重要性,
千家信息网最后更新 2024年11月12日如何使用Django和Prometheus以及Kubernetes定制应用指标

这篇文章给大家介绍如何使用Django和Prometheus以及Kubernetes定制应用指标,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。

这里强调了应用程序定制指标的重要性,用代码实例演示了如何设计指标并整合Prometheus到Django项目中,为使用Django构建应用的开发者提供了参考。

为什么自定义指标很重要?

尽管有大量关于这一主题的讨论,但应用程序的自定义指标的重要性怎么强调都不为过。和为Django应用收集的核心服务指标(应用和web服务器统计数据、关键数据库和缓存操作指标)不同,自定义指标是业务特有的数据点,其边界和阈值只有你自己知道,这其实是很有趣的事情。

什么样的指标才是有用的?考虑下面几点:

  • 运行一个电子商务网站并追踪平均订单数量。突然间订单的数量不那么平均了。有了可靠的应用指标和监控,你就可以在损失殆尽之前捕获到Bug。

  • 你正在写一个爬虫,它每小时从一个新闻网站抓取最新的文章。突然最近的文章并不新了。可靠的指标和监控可以更早地揭示问题所在。

  • 我认为你已经理解了重点。

设置Django应用程序

除了明显的依赖(pip install Django)之外,我们还需要为宠物项目(译者注:demo)添加一些额外的包。继续并安装pip install django-prometheus-client。这将为我们提供一个Python的Prometheus客户端,以及一些有用的Django hook,包括中间件和一个优雅的DB包装器。接下来,我们将运行Django管理命令来启动项目,更新我们的设置来使用Prometheus客户端,并将Prometheus的URL添加到URL配置中。

启动一个新的项目和应用程序

为了这篇文章,并且切合代理的品牌,我们建立了一个遛狗服务。请注意,它实际上不会做什么事,但足以作为一个教学示例。执行如下命令:

django-admin.py startproject demopython manage.py startapp walker
#settings.pyINSTALLED_APPS = [    ...    'walker',    ...]

现在,我们来添加一些基本的模型和视图。简单起见,我只实现将要验证的部分。如果想要完整地示例,可以从这个demo应用 获取源码。

# walker/models.pyfrom django.db import modelsfrom django_prometheus.models import ExportModelOperationsMixinclass Walker(ExportModelOperationsMixin('walker'), models.Model):    name = models.CharField(max_length=127)    email = models.CharField(max_length=127)    def __str__(self):        return f'{self.name} // {self.email} ({self.id})'class Dog(ExportModelOperationsMixin('dog'), models.Model):    SIZE_XS = 'xs'    SIZE_SM = 'sm'    SIZE_MD = 'md'    SIZE_LG = 'lg'    SIZE_XL = 'xl'    DOG_SIZES = (        (SIZE_XS, 'xsmall'),        (SIZE_SM, 'small'),        (SIZE_MD, 'medium'),        (SIZE_LG, 'large'),        (SIZE_XL, 'xlarge'),    )    size = models.CharField(max_length=31, choices=DOG_SIZES, default=SIZE_MD)    name = models.CharField(max_length=127)    age = models.IntegerField()    def __str__(self):        return f'{self.name} // {self.age}y ({self.size})'class Walk(ExportModelOperationsMixin('walk'), models.Model):    dog = models.ForeignKey(Dog, related_name='walks', on_delete=models.CASCADE)    walker = models.ForeignKey(Walker, related_name='walks', on_delete=models.CASCADE)    distance = models.IntegerField(default=0, help_text='walk distance (in meters)')    start_time = models.DateTimeField(null=True, blank=True, default=None)    end_time = models.DateTimeField(null=True, blank=True, default=None)    @property    def is_complete(self):        return self.end_time is not None    @classmethod    def in_progress(cls):        """ get the list of `Walk`s currently in progress """        return cls.objects.filter(start_time__isnull=False, end_time__isnull=True)    def __str__(self):        return f'{self.walker.name} // {self.dog.name} @ {self.start_time} ({self.id})'
# walker/views.pyfrom django.shortcuts import render, redirectfrom django.views import Viewfrom django.core.exceptions import ObjectDoesNotExistfrom django.http import HttpResponseNotFound, JsonResponse, HttpResponseBadRequest, Http404from django.urls import reversefrom django.utils.timezone import nowfrom walker import models, formsclass WalkDetailsView(View):    def get_walk(self, walk_id=None):        try:            return models.Walk.objects.get(id=walk_id)        except ObjectDoesNotExist:            raise Http404(f'no walk with ID {walk_id} in progress')class CheckWalkStatusView(WalkDetailsView):    def get(self, request, walk_id=None, **kwargs):        walk = self.get_walk(walk_id=walk_id)        return JsonResponse({'complete': walk.is_complete})class CompleteWalkView(WalkDetailsView):    def get(self, request, walk_id=None, **kwargs):        walk = self.get_walk(walk_id=walk_id)        return render(request, 'index.html', context={'form': forms.CompleteWalkForm(instance=walk)})    def post(self, request, walk_id=None, **kwargs):        try:            walk = models.Walk.objects.get(id=walk_id)        except ObjectDoesNotExist:            return HttpResponseNotFound(content=f'no walk with ID {walk_id} found')        if walk.is_complete:            return HttpResponseBadRequest(content=f'walk {walk.id} is already complete')        form = forms.CompleteWalkForm(data=request.POST, instance=walk)        if form.is_valid():            updated_walk = form.save(commit=False)            updated_walk.end_time = now()            updated_walk.save()            return redirect(f'{reverse("walk_start")}?walk={walk.id}')        return HttpResponseBadRequest(content=f'form validation failed with errors {form.errors}')class StartWalkView(View):    def get(self, request):        return render(request, 'index.html', context={'form': forms.StartWalkForm()})    def post(self, request):        form = forms.StartWalkForm(data=request.POST)        if form.is_valid():            walk = form.save(commit=False)            walk.start_time = now()            walk.save()            return redirect(f'{reverse("walk_start")}?walk={walk.id}')        return HttpResponseBadRequest(content=f'form validation failed with errors {form.errors}')

更新应用设置并添加Prometheus urls

现在我们有了一个Django项目以及相应的设置,可以为 django-prometheus添加需要的配置项了。在 settings.py中添加下面的配置:

INSTALLED_APPS = [    ...    'django_prometheus',    ...]MIDDLEWARE = [    'django_prometheus.middleware.PrometheusBeforeMiddleware',    ....    'django_prometheus.middleware.PrometheusAfterMiddleware',]# we're assuming a Postgres DB here because, well, that's just the right choice :)DATABASES = {    'default': {        'ENGINE': 'django_prometheus.db.backends.postgresql',        'NAME': os.getenv('DB_NAME'),        'USER': os.getenv('DB_USER'),        'PASSWORD': os.getenv('DB_PASSWORD'),        'HOST': os.getenv('DB_HOST'),        'PORT': os.getenv('DB_PORT', '5432'),    },}

添加url配置到 urls.py

urlpatterns = [    ...    path('', include('django_prometheus.urls')),]

现在我们有了一个配置好的基本应用,并为整合做好了准备。


添加Prometheus指标

由于django-prometheus提供了开箱即用功能,我们可以立即追踪一些基本的模型操作,比如插入和删除。可以在/metricsendpoint看到这些:

django-prometheus提供的默认指标

让我们把它变得更有趣点。

添加一个walker/metrics.py文件,定义一些要追踪的基本指标。

# walker/metrics.pyfrom prometheus_client import Counter, Histogramwalks_started = Counter('walks_started', 'number of walks started')walks_completed = Counter('walks_completed', 'number of walks completed')invalid_walks = Counter('invalid_walks', 'number of walks attempted to be started, but invalid')walk_distance = Histogram('walk_distance', 'distribution of distance walked', buckets=[0, 50, 200, 400, 800, 1600, 3200])

很简单,不是吗?Prometheus文档很好地解释了每种指标类型的用途,简言之,我们使用计数器来表示严格随时间增长的指标,使用直方图来追踪包含值分布的指标。下面开始验证应用的代码。

# walker/views.py...from walker import metrics...class CompleteWalkView(WalkDetailsView):    ...    def post(self, request, walk_id=None, **kwargs):        ...        if form.is_valid():            updated_walk = form.save(commit=False)            updated_walk.end_time = now()            updated_walk.save()            metrics.walks_completed.inc()            metrics.walk_distance.observe(updated_walk.distance)            return redirect(f'{reverse("walk_start")}?walk={walk.id}')        return HttpResponseBadRequest(content=f'form validation failed with errors {form.errors}')...class StartWalkView(View):    ...    def post(self, request):        if form.is_valid():            walk = form.save(commit=False)            walk.start_time = now()            walk.save()            metrics.walks_started.inc()            return redirect(f'{reverse("walk_start")}?walk={walk.id}')        metrics.invalid_walks.inc()        return HttpResponseBadRequest(content=f'form validation failed with errors {form.errors}')

发送几个样例请求,可以看到新指标已经产生了。

显示散步距离和创建散步的指标

定义的指标此时已经可以在prometheus里查找到了

至此,我们已经在代码中添加了自定义指标,整合了应用以追踪指标,并验证了这些指标已在/metrics 上更新并可用。让我们继续将仪表化应用部署到Kubernetes集群。

使用Helm部署应用

我只会列出和追踪、导出指标相关的配置内容,完整的Helm chart部署和服务配置可以在 demo应用中找到。 作为起点,这有一些和导出指标相关的deployment和configmap的配置:

# helm/demo/templates/nginx-conf-configmap.yamlapiVersion: v1kind: ConfigMapmetadata:  name: {{ include "demo.fullname" . }}-nginx-conf  ...data:  demo.conf: |    upstream app_server {      server 127.0.0.1:8000 fail_timeout=0;    }    server {      listen 80;      client_max_body_size 4G;      # set the correct host(s) for your site      server_name{{ range .Values.ingress.hosts }} {{ . }}{{- end }};      keepalive_timeout 5;      root /code/static;      location / {        # checks for static file, if not found proxy to app        try_files $uri @proxy_to_app;      }      location ^~ /metrics {        auth_basic           "Metrics";        auth_basic_user_file /etc/nginx/secrets/.htpasswd;        proxy_pass http://app_server;      }      location @proxy_to_app {        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_set_header X-Forwarded-Proto $scheme;        proxy_set_header Host $http_host;        # we don't want nginx trying to do something clever with        # redirects, we set the Host: header above already.        proxy_redirect off;        proxy_pass http://app_server;      }    }

# helm/demo/templates/deployment.yamlapiVersion: apps/v1kind: Deployment...    spec:      metadata:        labels:          app.kubernetes.io/name: {{ include "demo.name" . }}          app.kubernetes.io/instance: {{ .Release.Name }}          app: {{ include "demo.name" . }}      volumes:        ...        - name: nginx-conf          configMap:            name: {{ include "demo.fullname" . }}-nginx-conf        - name: prometheus-auth          secret:            secretName: prometheus-basic-auth        ...      containers:        - name: {{ .Chart.Name }}-nginx          image: "{{ .Values.nginx.image.repository }}:{{ .Values.nginx.image.tag }}"          imagePullPolicy: IfNotPresent          volumeMounts:            ...            - name: nginx-conf              mountPath: /etc/nginx/conf.d/            - name: prometheus-auth              mountPath: /etc/nginx/secrets/.htpasswd          ports:            - name: http              containerPort: 80              protocol: TCP        - name: {{ .Chart.Name }}          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"          imagePullPolicy: {{ .Values.image.pullPolicy }}          command: ["gunicorn", "--worker-class", "gthread", "--threads", "3", "--bind", "0.0.0.0:8000", "demo.wsgi:application"]          env:{{ include "demo.env" . | nindent 12 }}          ports:            - name: gunicorn              containerPort: 8000              protocol: TCP           ...

没什么神奇的,只是一些YAML而已。有两个重点需要强调一下:

  1. 我们通过一个nginx反向代理将/metrics放在了验证后面,为location块设置了auth_basic指令集。你可能希望在反向代理之后部署gunicorn ,但这样做可以获得保护指标的额外好处。

  2. 我们使用多线程的gunicorn而不是多个worker。虽然可以为Prometheus客户端启用多进程模式,但在Kubernetes环境中,安装会更为复杂。为什么这很重要呢?在一个pod中运行多个worker的风险在于,每个worker将在采集时报告自己的一组指标值。但是,由于服务在Prometheus Kubernetes SD scrape配置中被设置为pod级别 ,这些(潜在的)跳转值将被错误地分类为计数器重置,从而导致测量结果不一致。你并不一定需要遵循上述所有步骤,但重点是:如果你了解的不多,应该从一个单线程+单worker的gunicorn环境开始,或者从一个单worker+多线程环境开始。

使用Helm部署Prometheus

基于Helm的帮助文档,部署Prometheus非常简单,不需要额外工作:

helm upgrade --install prometheus stable/prometheus

几分钟后,你应该就可以通过 port-forward 进入Prometheus的pod(默认的容器端口是9090)。

为应用配置Prometheus scrape目标

Prometheus Helm chart 有大量的自定义可选项,不过我们只需要设置extraScrapeConfigs。创建一个values.yaml文件。你可以略过这部分直接使用 demo应用 作为参考。文件内容如下:

extraScrapeConfigs: |  - job_name: demo    scrape_interval: 5s    metrics_path: /metrics    basic_auth:      username: prometheus      password: prometheus    tls_config:      insecure_skip_verify: true    kubernetes_sd_configs:      - role: endpoints        namespaces:          names:            - default    relabel_configs:      - source_labels: [__meta_kubernetes_service_label_app]        regex: demo        action: keep      - source_labels: [__meta_kubernetes_endpoint_port_name]        regex: http        action: keep      - source_labels: [__meta_kubernetes_namespace]        target_label: namespace      - source_labels: [__meta_kubernetes_pod_name]        target_label: pod      - source_labels: [__meta_kubernetes_service_name]        target_label: service      - source_labels: [__meta_kubernetes_service_name]        target_label: job      - target_label: endpoint        replacement: http

创建完成后,就可以通过下面的操作为prometheus deployment更新配置。

helm upgrade --install prometheus -f values.yaml

为验证所有的步骤都配置正确了,打开浏览器输入 http://localhost:9090/targets (假设你已经通过 port-forward进入了运行prometheus的Pod)。如果你看到demo应用在target的列表中,说明运行正常了。

关于如何使用Django和Prometheus以及Kubernetes定制应用指标就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

0