1. 程式人生 > 實用技巧 >資料型別的有關資訊及位元組與其他度量單位的換算關係

資料型別的有關資訊及位元組與其他度量單位的換算關係

編者按

本文強調了應用程式定製指標的重要性,用程式碼例項演示瞭如何設計指標並整合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 demo
python manage.py startapp walker


#settings.py

INSTALLED_APPS = [
    ...
    'walker',
    ...
]

現在,我們來新增一些基本的模型和檢視。簡單起見,我只實現將要驗證的部分。如果想要完整地示例,可以從這個demo應用獲取原始碼。


# walker/models.py
from django.db import models
from django_prometheus.models import ExportModelOperationsMixin


class 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.py
from django.shortcuts import render, redirect
from django.views import View
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponseNotFound, JsonResponse, HttpResponseBadRequest, Http404
from django.urls import reverse
from django.utils.timezone import now
from walker import models, forms


class 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.py
from prometheus_client import Counter, Histogram


walks_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.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  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.yaml
apiVersion: apps/v1
kind: 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的列表中,說明執行正常了。

自己動手試試

我要強調一點:捕獲自定義的應用程式指標並設定相應的報告和監控是軟體工程中最重要的任務之一。幸運的是,將Prometheus指標整合到Django應用程式中實際上非常簡單,正如本文展示的那樣。如果你想要開始監測自己的應用,請參考完整的示例應用程式,或者直接fork程式碼庫。祝你玩得開心。