A Day In The Life

とあるプログラマの備忘録

Prometheus を使って Kubernetes 上に構築した Redis を監視する

PrometheusからRedisの状態を取得する

私は普段 Redis を GKE 上に構築して使用していますが、GCP コンソール画面から得られる情報が Pod の CPU 使用率、メモリ使用率、ディスク使用率くらいしかなくもう少し細かい情報を得たいと考えていました。そこで Prometheus を使って Redis の状態を監視してしてみたところとても良い感じだったのでその方法を紹介してみたいと思います。 Prometheus を使うと Redis の INFO コマンドで得られる情報を時系列に見ることができます。Prometheus は機能が豊富でいろいろできるのですが、この記事では必要最低限の設定でサクッとPrometheusを使った監視環境を構築をしてみたいと思います。

Prometheusって何?

Prometheus は SoundCloud が中心になって開発しているプル型の監視ツールです。既存のプログラムコードを変えることなくサクッと導入できるのでおすすめです。詳細は本家サイト Prometheus - Monitoring system & time series database か、 最近は専門の書籍も出ていますのでその辺りをチェックしてみてください 。

構成

以下の3つを Kubernetes 上に構築します。

  • Master/Slave構成のRedis
    • 監視対象のコンテナ
  • Exporter
    • Redisのメトリクス情報をPrometheusが読み込める形式に変換するコンテナ
  • Prometheus
    • 収集したRedisのメトリクス情報を時系列DBに保存して公開してくれるコンテナ

PrometheusはPull型の監視ツールのため、被監視対象のサーバからメトリクスを取得する必要があります。Prometheus は Redis から直接メトリクス情報を取得することができません。代わりに Exporterと呼ばれる Redis のメトリクス情報取得するためのアダプタ的なコンテナを Reids と同じ Pod に配置してそこから監視に必要な情報を取得します。

sequence dialog

Exporter はただの HTTP サーバです。Redis の各種メトリクス情報を Prometheus が解釈できるフォーマットに変換してくれます。Prometheus は Exporter が公開している /metrics にアクセスしてメトリクスを収集します。

構築の手順

構築の流れは以下のようになります。

  1. Prometheus を単体で構築する
  2. マスタースレーブ構成の Redis を構築する
  3. Redis の配置された Pod に Exporter を追加する
  4. Prometheus が Exporter からデータを収集できるように設定を修正する

前準備

本記事では Kubernetes 関連の yaml ファイルのデプロイに Kustomize を使います。以下のように k8s 関連の yaml を base という名前のディレクトリに格納するようにしてください。

base/
  ├ deployment.yaml
  ├ service.yaml
  └ ...etc

Prometheusを単体でk8s上に構築してみる

Prometheus を Kubernetes 上に構築するには Deployment と Service に加えて専用のコンフィグファイルをマウントする必要があります。

Deploymentの作成

はじめに Prometheus 本体の Pod リソースの設定をするため Deployment を作成します。Prometheus のコンテナは Docker Hub の prom/prometheus を使用します。 コンテナのポートは 9090 に設定します。stats という名前の Namespace 上に構築します(Namespaceの設定はあとでサービスと一緒に実施します)。

収集したメトリクス情報を保存するためのボリューム(data-volume)とConfigMap を読み込むためのボリューム(config-volume)をマウントしています。レプリカ数は1に設定します。

サービスの作成

Deployment が作成できたので Prometheus にアクセスするためのサービスを作成します。

今回は Service と一緒に Prometheus 専用の stats という名前の Namespace の定義もします。Service のタイプが LoadBalancer になっていますが、実際に運用する場合はロードバランサを internal にするか NodePort あたりを使ってください。

prometheus.yamlの作成

最後に Prometheus の設定をするためのコンフィグファイル(prometheus.yaml)を作成します。10秒間隔で Prometheus コンテナ自体の情報を収集するように設定してみます。この yaml ファイルは ConfigMap として読み込みます。

global:
  scrape_interval:     10s
  evaluation_interval: 10s
scrape_configs:
- job_name: 'prometheus'
  static_configs:
  - targets: ['localhost:9090']

global に Prometheus が情報を収集する時間間隔を定義し、scrape_configs に実施するジョブを定義しています。Prometheus は指定された間隔でジョブを実行してメトリクスデータを収集してくれます。

Prometheusをデプロイする

ここまで作成できるとファイル構成は以下のようになっています。

base/
  ├ prom-deployment.yaml
  ├ prom-service.yaml
  └ prometheus.yaml

base ディレクトリ以下に kustomize用のファイル kustomization.yaml を作成します。

resources:
- prom-service.yaml
- prom-deployment.yaml
configMapGenerator:
- name: prometheus-config
  files:
    - prometheus.yaml

kubectl コマンドを使って Prometheus をデプロイします。

kubectl apply -k base

ブラウザにURLhttp://35.1x9.1x0.XXX:9090/graphを入力して以下の画面が表示されればデプロイ成功です(35.1x9.1x0.XXXはサービスのIPアドレスです)。

f:id:glass-_-onion:20200421153349p:plain
Prometheus管理画面

Master/Slave構成のRedisを構築する

監視対象の Redis を Kubernetes 上に構築します。Headless Service を使ったシンプルなマスタースレーブ構成にしています。以前このブログで紹介した Redis Sentinel 構成から Sentinel 部分を取り払った構成になっています。Headless Service の説明もこちらの記事に書いてあります。

redis.confとRedisの起動ファイルを作成する

Redisの設定ファイル redis.conf ファイルを作成します。

bind 0.0.0.0
port 6379
maxclients 50000
dir /redis-data

Redisの起動時に実行されるスクリプトファイル launch.sh を作成します。

#!/bin/bash

SERVICE=redis
MASTER=${SERVICE}-0

if [ ${HOSTNAME} == ${MASTER} ]; then
    redis-server /config/redis.conf
else
    redis-server /config/redis.conf --slaveof ${MASTER}.${SERVICE} 6379
fi

StatefulSet の作成

Redis の Pod リソースの設定をするため StatefulSet を作成します。マスター1台、スレーブ2台構成にするのでレプリカ数は3に設定します。

StatefulSetを使うことで redis-0, redis-1, redis-2 という名前の Pod が生成されます。

Serviceの作成

Redis にアクセスするための Service を作成します。clusterIP に None を指定して Headless Service にします。

Headless Service にすることでクラスタ内から Pod名.サービス名 でアクセスすることが可能になります(今回の構成だとredis-0.redisのようになります)。

Redis をデプロイしてみる

以下の Kustomize ファイルを作成してデプロイしてみます。

resources:
- redis-service.yaml
- redis-ss.yaml
configMapGenerator:
- name: redis-config
  files:
    - launch.sh
    - redis.conf

kustomize.yaml を base ディレクトリに保存して以下のコマンドを実行します。

kubectl apply -k base

Redis にアクセスしてみる

redis-2 にアクセスして IP アドレスが返ってくるか確認します。

$ kubectl exec redis-2 -c redis \
-- redis-cli -p 26379 sentinel get-master-addr-by-name master

10.0.9.6
6379

Redis Exporterを追加する

ここまでで Prometheus 単体と Redis のマスタ/スレーブ がそれぞれ構築できました。ただこのままでは Prometheus は Redis のメトリクス情報を取得できません。そこで Redis の StatefulSet に Prometheus 用の Exporter コンテナを追加して Prometheus が Redis のメトリクスを取得できるようにします。頑張れば Redis の Exporter を自前で開発することもできますが割と大変なのでオープンソースの Redis Exporter を使います。Redis の Exporter は redis_exporter が有名なのでそちらを使ってみます。

先ほど作成した Redis の StatefulSet のファイル redis-ss.yaml を以下のように修正します。Redis の Pod に Redis Exporter をサイドカーとして配置します。

...省略...
      containers:
      - name: redis
        command: [sh, -c, source /config/launch.sh]
        image: redis:5-alpine
        ports:
        - containerPort: 6379
        volumeMounts:
        - mountPath: /config
          name: config
        - mountPath: /redis-data
          name: data
      - name: redis-exporter
        image: oliver006/redis_exporter:latest
        ports:
        - containerPort: 9121
...省略...

Exporter のポートは9121にします。Redis Exporter のコンテナの配置ができたので Prometheus が Exporter アクセスできるように Service にポートの設定を追加します。redis-service.yaml に以下の設定を追加します。

apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  type: ClusterIP
  clusterIP: None
  ports:
  - port: 6379
    name: port-redis
  - port: 9121
    name: port-redis-exporter
  selector:
    redis-app: redis

PrometheusからExporterの情報を収集する

ここまでで Redis 側の準備は整いました。あとは Prometheus が Redis Exporter にアクセスしてメトリクスを収集できるように設定するだけです。 Prometheus が Exporter からデータを取得するための設定を prometheus.yaml ファイルに書きます。Redis Exporter には Pod名.サービス名.ネームスペース名.svc:ポート番号 でアクセスすることができるようになります。prometheus.yaml の static_configs の targets を以下のように修正してください。

global:
  scrape_interval:     10s
  evaluation_interval: 10s
scrape_configs:
- job_name: redis_exporter
  static_configs:
  - targets:
    - redis-0.redis.default.svc:9121
    - redis-1.redis.default.svc:9121
    - redis-2.redis.default.svc:9121

Prometheus に Exporter を認識させる方法は static_configs とサービスディスカバリがありますが、今回は static_configs を使いました。サービスディスカバリの方が実用的ではあるものの RBAC の設定やサービスアカウントの作成が必要になり設定が複雑になるので static_configs にしました。

デプロイして Prometheus の管理画面から Redis の状態を確認する

Kustomize のファイルを以下のように修正して

resources:
- redis-service.yaml
- redis-ss.yaml
- prom-service.yaml
- prom-deployment.yaml
configMapGenerator:
- name: redis-config
  files:
    - launch.sh
    - redis.conf
- name: prometheus-config
  namespace: stats
  files:
    - prometheus.yaml

kustomize.yaml を base ディレクトリに保存して以下のコマンドを実行します。

kubectl apply -k base

デプロイが終わったら、ブラウザから Prometheus の管理画面(http://35.1x9.1x0.XXX:9090)にアクセスして Targets 画面に移動します。

f:id:glass-_-onion:20200421195319p:plain
Targets画面に移動する

以下のように Redis Exporter が認識されて入れば成功です。

f:id:glass-_-onion:20200421195357p:plain
Targets画面

試しに Redis の connected clients の情報を取得してみます。テキストボックスに redis_connected_clients と入力してから(またはドロップダウンから選択) Execute ボタンを押します。

f:id:glass-_-onion:20200421195714p:plain
redis_connected_clients

Redis の クライアント接続数が表示されました。取得したデータをグラフで見たい時は Graph タブを押すとグラフをみることができます。

f:id:glass-_-onion:20200421195859p:plain
グラフ

まとめ

Redis のコンテナと一緒に Redis Exporter のコンテナを配置すると Prometheus から Redis のメトリクスを取得することができます。Prometheusの コンフィグ設定は static_config を使うとお手軽です。

サンプルコード

今回のサンプルコードはこちらに置いています。

github.com

参考書籍

Prometheus について一番詳しく書かれている書籍です。

アダプタパターンとして Redis Exporter を使った事例が紹介されていました。

参考記事

関連記事

go-mode + lps-modeを使ってEmacsのGolang開発環境を整える

以前の記事 go-mode + eglotを使ってEmacsのGolang開発環境を整える - A Day In The Life で eglot を使った Go の開発環境構築方法を紹介しましたが、今回は lsp-mode を使って環境を構築してみたいと思います。前回と同じく Go の Language Server は gopls を使います。

動作確認バージョン

Emacsは25以上、Golangは1.11以上なら問題なく動作するはずです。

gopls のインストール

Go の Language Server は3種類あります(bingo 最近は開発を中止したみたいです)。

前回同様 gopls をインストールします。

$ go get -u golang.org/x/tools/cmd/gopls 

goimportsのインストール

こちらも前回同様、Go の import を整理してくれるコマンドもついでにインストールしておきます。

$ go get golang.org/x/tools/cmd/goimports

init.el の設定

入力補完は company-mode を使います。あと GOPATH や GOROOT など環境変数設定のために exec-path-from-shell を使います。

;; パッケージ管理サーバ
(require 'package)
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t)
(add-to-list 'package-archives '("marmalade" . "http://marmalade-repo.org/packages/"))
(package-initialize)
;; パッケージ情報の更新
(package-refresh-contents)

;; インストールするパッケージのリスト
(defvar my/favorite-packages
  '(
    use-package
    exec-path-from-shell
    ))
;; my/favorite-packagesからインストールしていないパッケージをインストール
(dolist (package my/favorite-packages)
  (unless (package-installed-p package)
    (package-install package)))

;; シェルに設定されている環境変数を引き継ぐ
(exec-path-from-shell-initialize)

;; lsp-mode
;; プロジェクトルートで M-x lsp-workspace-folder-add を実行すること
(use-package lsp-mode
  :ensure t ;自動インストール
  :custom ((lsp-inhibit-message t)
         (lsp-message-project-root-warning t)
         (create-lockfiles nil))
  :hook
  (prog-major-mode . lsp-prog-major-mode-enable)
  :config
  (setq lsp-response-timeout 5))
(add-hook 'hack-local-variables-hook
          (lambda () (when (derived-mode-p 'go-mode) (lsp))))

;; company-lsp integrates company mode completion with lsp-mode.
;; completion-at-point also works out of the box but doesn't support snippets.
(use-package company-lsp
  :ensure t
  :commands company-lsp)

;; Company mode is a standard completion package that works well with lsp-mode.
(use-package company
  :ensure t
  :config
  (global-company-mode)
  ;; Optionally enable completion-as-you-type behavior.
  (setq company-idle-delay 0)
  (setq company-minimum-prefix-length 1)
  (setq completion-ignore-case t)
  (setq company-dabbrev-downcase nil)
  (setq company-selection-wrap-around t))

;; go-mode
(use-package go-mode
  :ensure t
  :commands go-mode
  :config
  (setq gofmt-command "goimports")
  (add-hook 'before-save-hook 'gofmt-before-save))

emacs-lispのコードはこちらに置いてます。

プロジェクトルートが見つからない時の対策

Monorepo 構成で Go プロジェクトが複数存在する場合に lsp-mode がプロジェクトルートが判別できずローカルのパッケージの補完ができないことがあります。その場合は Emacs を起動後、以下のコマンドで追加します。

M-x lsp-workspace-folder-add

関連記事

参考

Kubernetes 上に Redis Sentinel を構築する

GCP環境でRedisを使う

GCP には Cloud Memorystore という Redis 互換のマネージドサービスがあるのですが、対応している Redis のバージョンが古い(4系)ので自前で GKE 上に Redis を立てることにしました。どうせ立てるなら master/slave 構成のほうが良いだろうということで Redis Sentinel にしました。

環境

  • GKE 1.13.6
  • Redis 5

Redis Sentinelって何?

Redis Sentinel は master/slave 構成の Redis の監視を行ってくれるサーバソフトウェアです。自動フェイルオーバー機能を持っています。

Redis Sentinelの構成

構成は以下のようになります。

sequence dialog

上の図では省略しましたが Sentinel は Sentinel 同士で繋がっていてすべての Redis を監視しています。

sequence dialog

Redisの設定

Redis の Config ファイルとコンテナ起動時に必要なスクリプトを作成します。

RedisのConfigファイルを作成する

Redis の master/slave 用の Config ファイルと Sentinel 用の Config ファイルを作成します。 はじめにマスター用の Config ファイル master.conf を作成します。

bind 0.0.0.0
port 6379

dir /redis-data

次にスレーブ用の Config ファイル slave.conf を作成します。

bind 0.0.0.0
port 6379

dir /redis-data

slaveof redis-0.redis 6379

最後に Sentinel 用の Config ファイル sentinel.conf を作成します。

bind 0.0.0.0
port 26379

sentinel monitor master redis-0.redis 6379 2
sentinel parallel-syncs master 1
sentinel down-after-milliseconds master 10000
sentinel failover-timeout master 20000

redis-0.redis をマスターに設定し sentinel でマスターを監視するように config に指定しています。 sentinel.confの設定内容については以下の記事が参考になります。

コンテナ起動時に実行するスクリプトを作成します

Redis コンテナ起動時に実行するスクリプト init.sh を作成します。どのコンテナをマスターにするか決定するためのものです。

#!/bin/bash
if [[ ${HOSTNAME} == 'redis-0' ]]; then
  redis-server /redis-config/master.conf
else
  redis-server /redis-config/slave.conf
fi

Sentinel 用のスクリプト sentinel.sh を作成します。マスターが立ち上がるのを待ってからredis-sentinelコマンドを実行します。

#!/bin/bash
while ! ping -c 1 redis-0.redis; do
    echo 'Waiting for server'
    sleep 1
done

redis-sentinel /redis-config/sentinel.conf

Redis 関連のファイルは以上で作成完了です。ファイル構成は以下のようになります。

|-- master.conf
|-- slave.conf
|-- sentinel.conf
|-- init.sh
`-- sentinel.sh

Redisの設定をConfigMapにまとめる

RedisのConfigファイルとコンテナ起動時に使うスクリプトをまとめて redis-config という名前の ConfigMap を作成します。

$ kubectl create configmap \
> --from-file=slave.conf=./slave.conf \
> --from-file=master.conf=./master.conf \
> --from-file=sentinel.conf=./sentinel.conf \
> --from-file=init.sh=./init.sh \
> --from-file=sentinel.sh=./sentinel.sh \
> redis-config

ワークロードの作成

次に Redis コンテナを定義する redis.yaml を作成します。デプロイメント(Deployment)ではなくステーフルセットを使うとポッドに ID が採番されポッド名-インデックスの形式でポッドにアクセスできます。replicas に3を指定しているので redis-0, redis-1, redis-2 という名前のポッド(Pod)が生成されます。Redis Sentinel 構成にする場合は Redis のコンテナが最低でも3つ以上必要なのとPodのIPアドレスをPod名から取得したかったのでこのような構成にしています。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  selector:
    matchLabels:
      app: redis
  replicas: 3
  serviceName: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      initContainers:
      - image: busybox:latest
        name: redis-init
        command: [sh, -c, cp -L /tmp/* /config]
        volumeMounts:
        - mountPath: /tmp
          name: tmp
        - mountPath: /config
          name: config
      containers:
      - command: [sh, -c, source /config/init.sh]
        image: redis:5-alpine
        name: redis
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - mountPath: /config
          name: config
        - mountPath: /redis-data
          name: data
      - command: [sh, -c, source /config/sentinel.sh]
        image: redis:5-alpine
        name: sentinel
        ports:
        - containerPort: 26379
          name: sentinel
        volumeMounts:
        - mountPath: /config
          name: config
      volumes:
      - configMap:
          name: redis-config
        name: tmp
      - emptyDir:
        name: config
      - emptyDir:
        name: data

サービスの作成

Redis にアクセスするためのサービスを作成します。clusterIP に None を設定することで Pod の IPアドレスを直接取得することができます。

apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  ports:
  - port: 6379
    name: port-redis
  - port: 26379
    name: port-sentinel
  clusterIP: None
  selector:
    app: redis

IP アドレスを設定しないサービスのことを Headless サービスと呼びます。今回のように Headless Service + StatefulSet の組み合わせで使うことが多いようです。Headless サービスの説明は以下を参照してください。

k8s 1.13以降はConfigMapが読み取り専用になったので注意が必要です

Redis Sentinel は Config ファイルにアクセスして内容を更新します。ConfigMap のままでは正常に動作しません。なのでボリュームに ConfigMap を読み込んでから書き込み可能な emptyDir なボリュームにファイルをコピーする必要があります。

# ファイルコピー部分の抜粋
initContainers:
- image: busybox:latest
  name: redis-init
  command: [sh, -c, cp -L /tmp/* /config]
  volumeMounts:
  - mountPath: /tmp #読み取り専用
    name: tmp
  - mountPath: /config #書き込み可能
    name: config

ConfigMap はボリューム作成時にシンボリックリンクとして作成されリンク先が読み取り専用に設定されます。なのでdefaultModeで書き込み権限を付与しても書き込みできるようにはなりません。

Redisのデプロイ

サービスをデプロイします。

$ kubectl apply -f service.yaml

ステートフルセットをデプロイします。

$ kubectl apply -f redis.yaml

確認

redis-2 にアクセスして IP アドレスが返ってくるか確認します。

$ kubectl exec redis-2 -c redis \
-- redis-cli -p 26379 sentinel get-master-addr-by-name master

10.0.9.6
6379

参考書籍

こちらの「入門 Kubernetes」の14章を参考にしました。

参考

ConfigMap の読み取り専用を回避する方法

sentinel.conf の設定

go-mode + eglotを使ってEmacsのGolang開発環境を整える

普段 Golang でサーバコードを書くときはもっぱら Emacs を使っています。Go Modules に移行してから gocode が動作しなくなったので最近はやりの LSP(Language Server Protocol) を導入することにしました。Go の Language Server は gopls 、Emacs のLSPクライアントは eglot を使います

動作確認バージョン

Emacsは25以上、Golangは1.11以上なら問題なく動作するはずです。

gopls のインストール

Go の Language Server は3種類あります。一番下の gopls が Golang の公式 Language Server です。

一長一短あるようですが公式が安心そうなので gopls をインストールします。

$ GO111MODULE=on go get -u golang.org/x/tools/cmd/gopls 

goimportsのインストール

Go の import を整理してくれるコマンドもついでにインストールしておきます。

$ go get golang.org/x/tools/cmd/goimports

init.el の設定

Emacs の LSP クライアントは lsp-mode が有名ですが、動作がめちゃくちゃ重かったのと余計な表示がいっぱい出てきたのでシンプルな eglot を使うことにしました。入力補完は company-mode を使います。あと GOPATH や GOROOT など環境変数設定のために exec-path-from-shell を使います。

;; パッケージ管理サーバ
(require 'package)
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t)
(add-to-list 'package-archives '("marmalade" . "http://marmalade-repo.org/packages/"))
(package-initialize)
;; パッケージ情報の更新
(package-refresh-contents)

;; インストールするパッケージのリスト
(defvar my/favorite-packages
  '(
    use-package
    exec-path-from-shell
    ))
;; my/favorite-packagesからインストールしていないパッケージをインストール
(dolist (package my/favorite-packages)
  (unless (package-installed-p package)
    (package-install package)))

;; シェルに設定されている環境変数を引き継ぐ
(exec-path-from-shell-initialize)

;; eglot
;; M-.で定義ジャンプ、M-,でジャンプ先からもどる
;; eglot はデフォルトの Language Server として go-langserver を使うので golsp に変更する
;; 事前に go get -u golang.org/x/tools/cmd/gopls しておくこと
(use-package eglot
  :ensure t ;自動インストール
  :config
  (define-key eglot-mode-map (kbd "M-.") 'xref-find-definitions)
  (define-key eglot-mode-map (kbd "M-,") 'pop-tag-mark)
  (add-to-list 'eglot-server-programs '(go-mode . ("gopls")))
  (add-hook 'go-mode-hook #'eglot-ensure))

;; company-mode
(use-package company
  :ensure t
  :config
  (global-company-mode)
  (setq company-idle-delay 0)
  (setq company-minimum-prefix-length 1)
  (setq company-dabbrev-downcase nil)
  (setq company-selection-wrap-around t))

(use-package go-mode
  :ensure t
  :commands go-mode
  :config
  (setq gofmt-command "goimports")
  (add-hook 'before-save-hook 'gofmt-before-save))

emacs-lispのコードはこちらに置いてます。

プロジェクトルートが見つからない時の対策

Monorepo 構成で Go プロジェクトが複数存在する場合に eglot がプロジェクトルートが判別できずローカルのパッケージの補完ができないことがあります(eglotはproject.elをつかってgitのファイル構成からルートを判別するようです)。その場合は projectile-mode を使ってプロジェクトルートを判別させます。init.elに以下のスクリプトを追加してから

;; Bridge projectile and project together so packages that depend on project
;; like eglot work
(use-package projectile
  :ensure t)
(defun my-projectile-project-find-function (dir)
  (let ((root (projectile-project-root dir)))
    (and root (cons 'transient root))))
(projectile-mode t)
(with-eval-after-load 'project
  (add-to-list 'project-find-functions 'my-projectile-project-find-function))

こんな感じでプロジェクトルートのディレクトリに空の . projectile ファイルを配置してください。

├ .git
├ foo_service
    ├ main.go
    └ .projectile
├ bar_service
    ├ main.go
    └ .projectile
└ baz_service
    ├ main.go
    └ .projectile

保管で Keyword argument のエラーが出た時の対処方法

error in process filter: Keyword argument :version not one of (:uri :diagnostics) のエラーが出るときは eglot のバージョンアップをすると解消します。

参考

Python を使って地域メッシュコードから緯度経度ポリゴンを算出する

日本の地域メッシュコードから緯度経度ポリゴンを算出する Python3.7 のプログラムを作成しました。 前回の記事の逆バージョンです。

プログラム

3次メッシュコード(基準地域メッシュコード)から緯度経度ポリゴンを算出します。精度を出すためにミリ秒で計算しています。

# ミリ秒
MILLISECOND = 3600000

class Grid(object):
    def __init__(self, code, parent=None, divide=0):
        size = len(code) // 2
        # コードの上位桁
        self.upper = int(code[:size])
        # コードの下位桁
        self.lower = int(code[size:])
        self.height_ms = MILLISECOND * (40 / 60)
        self.width_ms = MILLISECOND
        self.parent = parent
        if parent:
            self.height_ms = parent.height_ms / divide
            self.width_ms = parent.width_ms / divide

    def origin(self):
        if self.parent:
            p = self.parent.origin()
            lat_ms = p[0] + (self.height_ms * self.upper)
            lon_ms = p[1] + (self.width_ms * self.lower)
        else:
            # 親が存在しない=1次メッシュ
            lat_ms = self.upper * MILLISECOND / 1.5
            lon_ms = (self.lower + 100) * MILLISECOND
        return lat_ms, lon_ms


class Code2Polygon(object):
    def __init__(self, code):
        first_code = code[0:4]
        second_code = code[4:6]
        third_code = code[6:8]
        # 1次メッシュ
        self.__first = Grid(first_code)
        # 2次メッシュ(1次メッシュを8分割)
        self.__second = Grid(second_code, self.__first, 8)
        # 3次メッシュ(2次メッシュを10分割)
        self.__third = Grid(third_code, self.__second, 10)

    def __to_polygon(self, s, w, n, e):
        return [s, w], [s, e], [n, e], [n, w], [s, w]

    def __to_bounds(self, origin, height_ms, width_ms):
        south = origin[0]
        west = origin[1]
        north = south + height_ms
        east = west + width_ms
        return south / MILLISECOND, west / MILLISECOND, north / MILLISECOND, east / MILLISECOND

    def third_bounds(self):
        origin = self.__third.origin()
        return self.__to_bounds(origin, self.__third.height_ms, self.__third.width_ms)

    def third(self):
        t = self.third_bounds()
        return self.__to_polygon(t[0], t[1], t[2], t[3])

3次メッシュのみ算出してます。

使い方

Code2Polygon クラスのコンストラクタに地域メッシュコードを渡してポリゴンを取得します。

def test_code2polygon():
    # 渋谷付近のメッシュコード
    c2p = Code2Polygon('53393595')
    print(c2p.third())

上記プログラムの出力結果は以下のようになります。

([35.65833333333333, 139.6875], [35.65833333333333, 139.7], [35.666666666666664, 139.7], [35.666666666666664, 139.6875], [35.65833333333333, 139.6875])

参考記事

Python を使って緯度経度から地域メッシュコードを算出する

緯度経度(latitude, longitude)から日本の地域メッシュコードを算出する Python3.7 のプログラムを作成しました。

プログラム

緯度経度から1-3次メッシュコード(基準地域メッシュコード)を算出します。精度を出すためにミリ秒で計算しています。

import math

# 計算の単位(ミリ秒)
MILLISECOND = 3600000

class FirstGrid(object):
    def __init__(self, lat, lon):
        '''
        1次メッシュコード計算
        '''
        self.lat = lat
        self.lon = lon
        # メッシュの高さ
        self.height_ms = MILLISECOND * (40 / 60)
        # メッシュの幅
        self.width_ms = MILLISECOND
        # メッシュコードの上位桁
        self.upper = int(math.floor(lat * 15 / 10))
        # メッシュコードの下位桁
        self.lower = int(math.floor(lon - 100))
        # 南端(緯度)
        self.south_ms = self.upper * MILLISECOND / 1.5
        # 西端(経度)
        self.west_ms = (self.lower + 100) * MILLISECOND

    def code(self):
        ''' メッシュコード '''
        return f'{self.upper}{self.lower}'

    def origin(self):
        ''' メッシュの南西端(緯度経度) '''
        return self.south_ms / MILLISECOND, self.west_ms / MILLISECOND

class Grid(FirstGrid):
    def __init__(self, parent, divide):
        '''
        2,3次メッシュコード計算
        :param parent 親メッシュ
        :param divide 分割単位
        '''
        self.lat = parent.lat
        self.lon = parent.lon
        lat_ms = parent.lat * MILLISECOND
        lon_ms = parent.lon * MILLISECOND
        # 親メッシュの高さと幅から当該メッシュの高さと幅を算出する
        self.height_ms = parent.height_ms / divide
        self.width_ms = parent.width_ms / divide
        h = self.height_ms
        w = self.width_ms
        # 上位桁
        self.upper = int(math.floor((lat_ms - parent.south_ms) / h))
        # 下位桁
        self.lower = int(math.floor((lon_ms - parent.west_ms) / w))
        # 南端
        self.south_ms = self.upper * h + parent.south_ms
        # 西端
        self.west_ms = self.lower * w + parent.west_ms

    def code(self):
        return f'{self.upper}{self.lower}'

    def origin(self):
        return self.south_ms / MILLISECOND, self.west_ms / MILLISECOND

class LatLon2Code(object):
    def __init__(self, lat, lon):
        f = FirstGrid(lat, lon)
        # 2次メッシュは1次メッシュを8分割する
        s = Grid(f, 8)
        self.__first = f
        self.__second = s
        # 3次メッシュは2次メッシュを10分割する
        self.__third = Grid(s, 10)

    def first(self):
        return self.__first.code()

    def second(self):
        return f'{self.__first.code()}{self.__second.code()}'

    def third(self):
        return f'{self.__first.code()}{self.__second.code()}{self.__third.code()}'

地域メッシュは英語だと Grid Square と表現するらしいのでクラス名は Grid にしています。1次メッシュの計算が特殊で2次3次は同じ計算をするので、1次と2次3次でクラスを分けました。

使い方

LatLon2Code クラスのコンストラクタに緯度経度を渡してコードを取得します。

def test_latlon2code():
    # 渋谷
    ll2c = LatLon2Code(35.6640352, 139.6982122)
    assert(ll2c.first() == '5339')
    assert(ll2c.second() == '533935')
    assert(ll2c.third() == '53393595')

参考記事

PostGISでgeometry型からgeography型に変換するときは座標系に注意する

SRID=4612(JGD2000測地系+地理座標系)のgeometry型フィールドだとgeography型に変換できる

postgres=> SELECT ST_SetSRID(geom, 4612)::geography FROM map WHERE id=1;
                     st_setsrid
----------------------------------------------------
 0101000020041200006B405577CB756140404749F6A1BF4140

SRID=3857(WGS84測地系球面+メルカトル図法)のgeometry型フィールドをgeography型に変換しようとするとエラーになる

postgres=> SELECT ST_SetSRID(geom, 3857)::geography FROM map WHERE id=1;
ERROR:  Only lon/lat coordinate systems are supported in geography.

EPSG: 3857は投影座標系で単位がメートルなのでダイレクトに変換できないみたいです。

参考