Zhangjian He
← Blog

微服务亲和反亲和部署模式下的日志打印分析

灵活部署要求

常见的微服务开发下,可能会对微服务的日志打印有一些要求.常见的一种模式是,将日志文件挂载在主机路径中,然后在主机上启动filebeat收集日志.

以我的一个demo工程rabbitmq-adapt为例:

容器内打印路径: /opt/sh/logs/file.log
容器外挂载路径: /opt/log/rabbitmq/file.log

但是这样子,如果我们需要在一个虚拟机上跑两个rabbitmq-adapt的时候,日志路径会重复,这种情况下,不仅我们在虚拟机很难定位问题,而且filebeat也很难给日志标记上不同实例的标签.

所以首先,我们得把日志路径按pod实例的方式隔离,选择k8s中HOSTNAME环境变量作为文件路径前缀是一个不错的选择

akka@AkkadeMacBook-Pro ~ % kubectl exec -it rabbitmq-58c5f4ff6b-zthg4 bash
[root@rabbitmq-58c5f4ff6b-zthg4 /]# env
LANG=en_US.UTF-8
HOSTNAME=rabbitmq-58c5f4ff6b-zthg4
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_PORT=tcp://10.96.0.1:443
POD_NAME=rabbitmq-58c5f4ff6b-zthg4
PWD=/
HOME=/root
NODE_NAME=minikube
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
TERM=xterm
SHLVL=1
KUBERNETES_SERVICE_PORT=443
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
POD_IP=172.17.0.2
KUBERNETES_SERVICE_HOST=10.96.0.1
LESSOPEN=||/usr/bin/lesspipe.sh %s
_=/usr/bin/env

k8s subPathExpr方案: 查阅资料,官方推荐的是这个方案

这个时候,你的映射关系是 /opt/sh/logs <==> /opt/log/rabbitmq/${POD_NAME}

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rabbitmq
  labels:
    app: rabbitmq
spec:
  replicas: 2
  selector:
    matchLabels:
      app: rabbitmq
  template:
    metadata:
      labels:
        app: rabbitmq
    spec:
      containers:
      - name: rabbitmq
        env:
        - name: NODE_NAME
          valueFrom:
            fieldRef:
                fieldPath: spec.nodeName        
        - name: POD_NAME
          valueFrom:
            fieldRef:
                fieldPath: metadata.name            
        - name: POD_IP
          valueFrom:
            fieldRef:
                fieldPath: status.podIP
        image: hezhangjian/rabbitmq-adapt:0.0.1
        readinessProbe:
          httpGet:
            path: /readiness
            port: 8083
          initialDelaySeconds: 3
          periodSeconds: 3
        volumeMounts:
          - mountPath: /opt/sh/log
            name: rabbitmq-log
            # subPath的方案很好,但对版本号要求很高,>=1.14
            subPathExpr: $(POD_NAME)
        resources:
          limits:
            memory: 4G
            cpu: 1000m
          requests:
            memory: 500M
            cpu: 250m
        securityContext:
            privileged: true
      volumes:
    #   - name: rabbitmq-data
    #     hostPath: 
    #       path: "/Users/akka/rabbitmq"
    #       type: DirectoryOrCreate        
      - name: rabbitmq-log
        hostPath:
          path: /opt/log/rabbitmq
          type: DirectoryOrCreate

可以达到如下的效果:

minikube/                  rabbitmq-6df8f7565c-kh2h6/ rabbitmq-6df8f7565c-ss92l/

log4j2环境变量隔离方案:适用于在你的k8s版本还不够的情况下

这个时候,你的文件映射关系还是 /opt/sh/logs <==> /opt/log/rabbitmq

但是真正在打印日志的时候,把日志都打印到/opt/sh/logs/${POD_NAME}下,这样子也可以在一台vm上跑多个实例

<?xml version="1.0" encoding="UTF-8" ?>

<Configuration status="warn" monitorInterval="10">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd,HH:mm:ss,SSSXXX}(%r):%4p%X[%t#%T]%l-->%m%n"/>
        </Console>
        <File name="FILE" fileName="${env:HOSTNAME}/file.log">
            <PatternLayout pattern="%d{yyyy-MM-dd,HH:mm:ss,SSSXXX}(%r):%4p%X[%t#%T]%l-->%m%n"/>
        </File>
    </Appenders>
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="FILE"/>
        </Root>
    </Loggers>
</Configuration>

我们解决了两个容器在一个vm上打印日志的问题,紧接着,我们要分析filebeat的能力,filebeat能否区分这两个路径,把这两个路径打上不同的标签

运行并配置好filebeat,配置文件如下

filebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /opt/sh/collect/log/es/*/*.log
    tags: ["es"]
filebeat.config.modules:
  path: ${path.config}/modules.d/*.yml
  reload.enabled: false
  reload.period: 10s
setup.template.settings:
  index.number_of_shards: 1
fields:
  fields_under_root: true
setup.kibana:
output.elasticsearch:
  hosts: ["localhost:9200"]
processors:
  - add_host_metadata: ~
  - add_docker_metadata: ~
  - add_kubernetes_metadata:
      kube_config: /opt/sh/collect/log/config
logging.level: debug

,查询收集上来的数据

curl 127.0.0.1:9200/filebeat-7.5.1-2020.01.23-000001/_search?pretty
收集上来的数据存在hostname字段,能区分代表单个实例的信息.

使用ES processor添加实例id信息

curl -X PUT "localhost:9200/_ingest/pipeline/attach_instance?pretty" -H 'Content-Type: application/json' -d'
{
  "processors": [
    {
      "grok": {
        "field": "log.file.path",
        "patterns": ["/opt/sh/collect/log/es/%{WORD:instanceId}/es.log"]
      }
    }
  ]
}
'

然后修改filebeat配置文件

filebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /opt/sh/collect/log/es/*/*.log
    tags: ["es"]
filebeat.config.modules:
  path: ${path.config}/modules.d/*.yml
  reload.enabled: false
  reload.period: 10s
setup.template.settings:
  index.number_of_shards: 1
fields:
  fields_under_root: true
setup.kibana:
output.elasticsearch:
  hosts: ["localhost:9200"]
  pipeline: attach_instance
processors:
  - add_host_metadata: ~
  - add_docker_metadata: ~
  - add_kubernetes_metadata:
      kube_config: /opt/sh/collect/log/config
logging.level: debug

查询结果就会出现在instanceId字段

总结

三种方案均可以方便地实现同一vm上部署两个容器. 方案一只需要修改tosca模板,但要1.14版本才支持.方案二需要修改少量代码.

但前两种均不适合配置了hostnetwork,即独占主机的网络,原因,主机网络独占之后,两个容器的hostname都相同,实际中,已经独占网络的容器,还需要部署在一个节点的需求,应该比较少.

如果有,可以使用在es处处理文件路径,加上instanceId字段